diff --git a/.gitignore b/.gitignore index c4d839d0398..f1160bc4c32 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,7 @@ integration-tests/spreadsheet/files/exel-multi-part_4.xlsx integration-tests/json/output integration-tests/mdi/files/excelwriter-mdi-test.xlsx integration-tests/spreadsheet/files/sample-file-append.xlsx +integration-tests/spark-native/output assemblies/debug/audit @@ -66,11 +67,8 @@ rap/src/main/resources/org/apache/hop/ui/hopgui/monaco/monaco-files.list # Screenshots captured by SWTbot screenshots/ -<<<<<<< Updated upstream # Docker script leaving audit data docker/local-data/ -======= -/docker/local-data/ ->>>>>>> Stashed changes +/integration-tests/spreadsheet/files/exelwriter-testfile diff --git a/NOTICE b/NOTICE index dfff7c42117..3c525898df6 100644 --- a/NOTICE +++ b/NOTICE @@ -9,3 +9,13 @@ Hitachi Vantara (https://www.hitachivantara.com//). This product includes software developed by JADAPTIVE (https://www.jadaptive.com/) + +This product bundles Delta Lake (io.delta) under the Apache License, Version 2.0. +See: https://github.com/delta-io/delta + +This product bundles Apache Iceberg under the Apache License, Version 2.0. +See: https://iceberg.apache.org/ + +This product bundles Unity Catalog client libraries (transitive dependency of +Delta Lake) under the Apache License, Version 2.0. +See: https://github.com/unitycatalog/unitycatalog diff --git a/assemblies/client/pom.xml b/assemblies/client/pom.xml index 1adf6ccf456..c7e1dd1c21e 100644 --- a/assemblies/client/pom.xml +++ b/assemblies/client/pom.xml @@ -67,6 +67,12 @@ ${project.version} zip + + org.apache.hop + hop-engines-spark + ${project.version} + zip + org.apache.hop hop-ui diff --git a/assemblies/debug/pom.xml b/assemblies/debug/pom.xml index 8036f69593a..f734be09fa1 100644 --- a/assemblies/debug/pom.xml +++ b/assemblies/debug/pom.xml @@ -355,6 +355,12 @@ ${project.version} provided + + org.apache.hop + hop-engines-spark + ${project.version} + provided + org.apache.hop hop-misc-git diff --git a/assemblies/plugins/pom.xml b/assemblies/plugins/pom.xml index 6fc89979ce0..30d0e6d0f52 100644 --- a/assemblies/plugins/pom.xml +++ b/assemblies/plugins/pom.xml @@ -618,6 +618,12 @@ ${project.version} zip + + org.apache.hop + hop-engines-spark + ${project.version} + zip + org.apache.hop hop-misc-async diff --git a/core/src/main/java/org/apache/hop/core/gui/plugin/GuiElementType.java b/core/src/main/java/org/apache/hop/core/gui/plugin/GuiElementType.java index f7b5af4fbd6..afe04a8c706 100644 --- a/core/src/main/java/org/apache/hop/core/gui/plugin/GuiElementType.java +++ b/core/src/main/java/org/apache/hop/core/gui/plugin/GuiElementType.java @@ -20,6 +20,11 @@ public enum GuiElementType { NONE, // To disable default options TEXT, + /** + * Multi-line text widget ({@code SWT.MULTI}). Height in lines is set via {@link + * GuiWidgetElement#multiLineTextHeight()}. + */ + MULTI_LINE_TEXT, FILENAME, // Text widget with browse button FOLDER, // Text widget with browse button COMBO, diff --git a/core/src/main/java/org/apache/hop/core/gui/plugin/GuiElements.java b/core/src/main/java/org/apache/hop/core/gui/plugin/GuiElements.java index d49c67bf5a6..0c773a3c6e4 100644 --- a/core/src/main/java/org/apache/hop/core/gui/plugin/GuiElements.java +++ b/core/src/main/java/org/apache/hop/core/gui/plugin/GuiElements.java @@ -49,6 +49,9 @@ public class GuiElements extends BaseGuiElements implements Comparable fieldClass; @@ -104,6 +107,7 @@ public GuiElements(GuiWidgetElement guiElement, Field field) { this.disabledImage = null; this.variablesEnabled = guiElement.variables(); this.password = guiElement.password(); + this.multiLineTextHeight = Math.max(1, guiElement.multiLineTextHeight()); this.ignored = guiElement.ignored(); this.addingSeparator = guiElement.separator(); this.label = getTranslation(guiElement.label(), fieldPackageName, field.getDeclaringClass()); @@ -143,6 +147,7 @@ public GuiElements(GuiWidgetElement guiElement, Method method, ClassLoader class this.disabledImage = null; this.variablesEnabled = guiElement.variables(); this.password = guiElement.password(); + this.multiLineTextHeight = Math.max(1, guiElement.multiLineTextHeight()); this.ignored = guiElement.ignored(); this.addingSeparator = guiElement.separator(); this.label = getTranslation(guiElement.label(), methodPackageName, method.getDeclaringClass()); @@ -370,6 +375,22 @@ public void setPassword(boolean password) { this.password = password; } + /** + * Preferred height for {@link GuiElementType#MULTI_LINE_TEXT}, in text lines (at least 1). + * + * @return value of multiLineTextHeight + */ + public int getMultiLineTextHeight() { + return multiLineTextHeight; + } + + /** + * @param multiLineTextHeight The multiLineTextHeight to set (clamped to at least 1) + */ + public void setMultiLineTextHeight(int multiLineTextHeight) { + this.multiLineTextHeight = Math.max(1, multiLineTextHeight); + } + /** * Gets fieldName * diff --git a/core/src/main/java/org/apache/hop/core/gui/plugin/GuiWidgetElement.java b/core/src/main/java/org/apache/hop/core/gui/plugin/GuiWidgetElement.java index d52cab091f8..3d9adda3e9b 100644 --- a/core/src/main/java/org/apache/hop/core/gui/plugin/GuiWidgetElement.java +++ b/core/src/main/java/org/apache/hop/core/gui/plugin/GuiWidgetElement.java @@ -80,6 +80,14 @@ */ boolean password() default false; + /** + * Preferred height of a {@link GuiElementType#MULTI_LINE_TEXT} widget, in text lines (not + * pixels). Default is 1. Values less than 1 are treated as 1. Ignored for other element types. + * + * @return height in lines + */ + int multiLineTextHeight() default 1; + /** * @return true if the widget supports variables */ diff --git a/dist/spark-native-demo/README.md b/dist/spark-native-demo/README.md new file mode 100644 index 00000000000..0ec658ac4e1 --- /dev/null +++ b/dist/spark-native-demo/README.md @@ -0,0 +1,33 @@ + + +# Staging location moved + +Native Spark demo artifacts default to **`/tmp/spark-demo-dist`** (not under the git tree). + +```bash +./plugins/engines/spark/src/samples/spark-demo/scripts/prepare-dist.sh +# → /tmp/spark-demo-dist/{hop-native-spark4-submit.jar,spark-demo.zip,hop-data/,…} + +docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml \ + up -d --build --scale spark-worker=2 +# hop-data is bind-mounted to /data/hop-data in the cluster (outputs + executions on the host) +``` + +Override with `DIST_DIR` / `HOP_DIST_DIR` / `HOP_DATA_HOST_DIR` if you prefer other paths. diff --git a/docker/integration-tests/integration-tests-base.yaml b/docker/integration-tests/integration-tests-base.yaml index 2f1c9277c1c..4bb32ece032 100644 --- a/docker/integration-tests/integration-tests-base.yaml +++ b/docker/integration-tests/integration-tests-base.yaml @@ -33,4 +33,6 @@ services: - FLASK_ENV=docker # Optional workflow basename filter (substring or glob). Set by run-tests-docker.sh. - TEST_FILTER=${TEST_FILTER:-} + # Skip Google Sheets ITs when no real service-account JSON key is provided. + - SKIP_GOOGLE_SHEETS=${SKIP_GOOGLE_SHEETS:-false} command: [ "bash", "-c", "/files/scripts/run-tests.sh ${PROJECT_NAME}" ] diff --git a/docker/integration-tests/integration-tests-beam-base.yaml b/docker/integration-tests/integration-tests-beam-base.yaml index 8521fd7ee89..f7c82ecdf07 100644 --- a/docker/integration-tests/integration-tests-beam-base.yaml +++ b/docker/integration-tests/integration-tests-beam-base.yaml @@ -35,4 +35,5 @@ services: - HOP_SPARK_CLIENT_VERSION=${HOP_SPARK_CLIENT_VERSION:-} # Optional workflow basename filter (substring or glob). Set by run-tests-docker.sh. - TEST_FILTER=${TEST_FILTER:-} + - SKIP_GOOGLE_SHEETS=${SKIP_GOOGLE_SHEETS:-false} command: [ "bash", "-c", "/files/scripts/run-tests.sh ${PROJECT_NAME}" ] diff --git a/docker/integration-tests/integration-tests-iceberg.yaml b/docker/integration-tests/integration-tests-iceberg.yaml new file mode 100644 index 00000000000..f7bd3a3e45e --- /dev/null +++ b/docker/integration-tests/integration-tests-iceberg.yaml @@ -0,0 +1,27 @@ +# 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. + +# Apache Iceberg table ITs on native SparkPipelineEngine (local[2]). +# Requires engines-spark with lib/iceberg (default assembly). +services: + hop-iceberg-test: + extends: + file: integration-tests-beam-base.yaml + service: integration_test + environment: + - FLASK_ENV=docker + - HOP_OPTIONS=-Dspark.ui.enabled=false diff --git a/docker/integration-tests/integration-tests-lakehouse.yaml b/docker/integration-tests/integration-tests-lakehouse.yaml new file mode 100644 index 00000000000..17e9bb4451c --- /dev/null +++ b/docker/integration-tests/integration-tests-lakehouse.yaml @@ -0,0 +1,27 @@ +# 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. + +# Delta Lake table ITs on native SparkPipelineEngine (local[2]). +# Requires engines-spark with lib/delta (default assembly). +services: + hop-lakehouse-test: + extends: + file: integration-tests-beam-base.yaml + service: integration_test + environment: + - FLASK_ENV=docker + - HOP_OPTIONS=-Dspark.ui.enabled=false diff --git a/docker/integration-tests/integration-tests-spark-native-cluster.yaml b/docker/integration-tests/integration-tests-spark-native-cluster.yaml new file mode 100644 index 00000000000..bb9750bfd88 --- /dev/null +++ b/docker/integration-tests/integration-tests-spark-native-cluster.yaml @@ -0,0 +1,105 @@ +# 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. + +# Native Spark 4.1 standalone cluster for the spark-demo walk-through. +# +# Proves multi-node spark-submit with a Hop project package (Simple Mapping / +# Workflow Executor under ${PROJECT_HOME}) and shared data on a host-visible +# bind mount (HOP_DATA=file:///data/hop-data inside the containers). +# +# Host data plane (default): ${HOP_DIST_DIR:-/tmp/spark-demo-dist}/hop-data +# → container /data/hop-data (CSV seeds, out/, packages/, executions/) +# +# From the repository root: +# +# # Prepare fat jar + package zip into /tmp/spark-demo-dist (see sample scripts) +# ./plugins/engines/spark/src/samples/spark-demo/scripts/prepare-dist.sh +# +# docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml \ +# up -d --build --scale spark-worker=2 +# +# # Mapping demo (default) +# docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml \ +# exec spark /opt/hop-samples/spark-demo/scripts/spark-submit-demo.sh +# +# # Workflow Executor country folders demo +# docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml \ +# exec spark /opt/hop-samples/spark-demo/scripts/spark-submit-workflows-demo.sh +# +# # Nested Native Spark pipelines (per country) demo +# docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml \ +# exec spark /opt/hop-samples/spark-demo/scripts/spark-submit-pipelines-demo.sh +# +# # Inspect on the host (no docker exec required) +# ls -la /tmp/spark-demo-dist/hop-data/out /tmp/spark-demo-dist/hop-data/executions +# +# Spark master UI: http://localhost:8080 +# Override SPARK_VERSION if needed (default matches plugins/engines/spark Spark 4.1.x pin). +# Override data host path: HOP_DATA_HOST_DIR=/somewhere/else docker compose … + +services: + spark: + build: + context: ./spark + dockerfile: Dockerfile.master + args: + SPARK_VERSION: ${SPARK_VERSION:-4.1.2} + HADOOP_VERSION: ${HADOOP_VERSION:-3} + BASE_URL: ${SPARK_BASE_URL:-https://dlcdn.apache.org/spark} + hostname: spark + environment: + - INIT_DAEMON_STEP=setup_spark + - SPARK_MASTER_HOST=spark + ports: + - "8080:8080" + - "7077:7077" + volumes: + # Host-prepared fat jar + project package zip + - ${HOP_DIST_DIR:-/tmp/spark-demo-dist}:/opt/hop-dist:ro + # Sample project sources + scripts + - ../../plugins/engines/spark/src/samples:/opt/hop-samples:ro + # Shared data plane (master + workers): bind-mounted so the host can inspect + # outputs and execution-info files without docker exec. + # Default: /tmp/spark-demo-dist/hop-data (override HOP_DATA_HOST_DIR; if you change + # HOP_DIST_DIR, set HOP_DATA_HOST_DIR=${HOP_DIST_DIR}/hop-data as well) + - ${HOP_DATA_HOST_DIR:-/tmp/spark-demo-dist/hop-data}:/data/hop-data + healthcheck: + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/"] + interval: 5s + timeout: 5s + retries: 24 + start_period: 40s + + spark-worker: + build: + context: ./spark + dockerfile: Dockerfile.worker + args: + SPARK_VERSION: ${SPARK_VERSION:-4.1.2} + HADOOP_VERSION: ${HADOOP_VERSION:-3} + BASE_URL: ${SPARK_BASE_URL:-https://dlcdn.apache.org/spark} + depends_on: + spark: + condition: service_healthy + environment: + - SPARK_MASTER=spark://spark:7077 + - SPARK_WORKER_CORES=${SPARK_WORKER_CORES:-2} + - SPARK_WORKER_MEMORY=${SPARK_WORKER_MEMORY:-2g} + volumes: + - ${HOP_DATA_HOST_DIR:-/tmp/spark-demo-dist/hop-data}:/data/hop-data + ports: + - "8081" diff --git a/docker/integration-tests/integration-tests-spark-native.yaml b/docker/integration-tests/integration-tests-spark-native.yaml new file mode 100644 index 00000000000..88273183859 --- /dev/null +++ b/docker/integration-tests/integration-tests-spark-native.yaml @@ -0,0 +1,29 @@ +# 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. + +# Native SparkPipelineEngine smoke (local[2] only — no Spark cluster). +# Extends hop-beam-image so /tmp/hop-fatjar.jar is present for future cluster tests +# (spark-local run config leaves fatJar empty for Spark 4.1 plugin classpath). +services: + hop-spark-native-test: + extends: + file: integration-tests-beam-base.yaml + service: integration_test + environment: + - FLASK_ENV=docker + # Avoid Spark UI / jersey servlet clash on the driver + - HOP_OPTIONS=-Dspark.ui.enabled=false diff --git a/docker/integration-tests/spark/scripts/master.sh b/docker/integration-tests/spark/scripts/master.sh index 8d225dbb33e..85a77910009 100644 --- a/docker/integration-tests/spark/scripts/master.sh +++ b/docker/integration-tests/spark/scripts/master.sh @@ -32,5 +32,6 @@ ln -sf /dev/stdout $SPARK_MASTER_LOG/spark-master.out # # start history server and thrift server # /spark/sbin/start-thriftserver.sh +# Spark 3 accepted --ip; Spark 4 Master only accepts --host / -h (see Master --help). cd /spark/bin && /spark/sbin/../bin/spark-class org.apache.spark.deploy.master.Master \ - --ip $SPARK_MASTER_HOST --port $SPARK_MASTER_PORT --webui-port $SPARK_MASTER_WEBUI_PORT >> $SPARK_MASTER_LOG/spark-master.out \ No newline at end of file + --host $SPARK_MASTER_HOST --port $SPARK_MASTER_PORT --webui-port $SPARK_MASTER_WEBUI_PORT >> $SPARK_MASTER_LOG/spark-master.out \ No newline at end of file diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/databricks-jobs-pipelines-hello-databricks-initializing.png b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/databricks-jobs-pipelines-hello-databricks-initializing.png new file mode 100644 index 00000000000..4cdf9f6c65e Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/databricks-jobs-pipelines-hello-databricks-initializing.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/databricks-jobs-pipelines-hello-databricks-memory-usage-4gb.png b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/databricks-jobs-pipelines-hello-databricks-memory-usage-4gb.png new file mode 100644 index 00000000000..03bdb89426c Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/databricks-jobs-pipelines-hello-databricks-memory-usage-4gb.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/databricks-jobs-pipelines-hello-databricks-running.png b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/databricks-jobs-pipelines-hello-databricks-running.png new file mode 100644 index 00000000000..f1a2e1de0b3 Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/databricks-jobs-pipelines-hello-databricks-running.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/databricks-jobs-pipelines-hello-databricks-succeeded.png b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/databricks-jobs-pipelines-hello-databricks-succeeded.png new file mode 100644 index 00000000000..113b4d07fd8 Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/databricks-jobs-pipelines-hello-databricks-succeeded.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/databricks-pipeline-hello-databricks.png b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/databricks-pipeline-hello-databricks.png new file mode 100644 index 00000000000..cf56adaed82 Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/databricks-pipeline-hello-databricks.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/databricks-workflow-deploy-to-databricks.png b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/databricks-workflow-deploy-to-databricks.png new file mode 100644 index 00000000000..a6b57bb1079 Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/databricks-workflow-deploy-to-databricks.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/databricks-workspace-catalog-jars-volume.png b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/databricks-workspace-catalog-jars-volume.png new file mode 100644 index 00000000000..272708a0aa9 Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/databricks-workspace-catalog-jars-volume.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/native-spark-logs-to-execution-information-location.png b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/native-spark-logs-to-execution-information-location.png new file mode 100644 index 00000000000..3f561d00144 Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/native-spark-logs-to-execution-information-location.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/native-spark-pipeline-engine-run-configurtion-editor.png b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/native-spark-pipeline-engine-run-configurtion-editor.png new file mode 100644 index 00000000000..c7363d9c622 Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/native-spark-pipeline-engine-run-configurtion-editor.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/native-spark-provides-runtime-transform-metrics-and-state.png b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/native-spark-provides-runtime-transform-metrics-and-state.png new file mode 100644 index 00000000000..e24c7f61147 Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/native-spark-provides-runtime-transform-metrics-and-state.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/native-spark-run-mode-force-driver.png b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/native-spark-run-mode-force-driver.png new file mode 100644 index 00000000000..8cbdb508627 Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/native-spark-run-mode-force-driver.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0001-input-output.png b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0001-input-output.png new file mode 100644 index 00000000000..a725ab534d9 Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0001-input-output.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0002-group-by.png b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0002-group-by.png new file mode 100644 index 00000000000..0995d4b4cd0 Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0002-group-by.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0003-filter-rows.png b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0003-filter-rows.png new file mode 100644 index 00000000000..6e2cea4c725 Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0003-filter-rows.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0004-stream-lookup.png b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0004-stream-lookup.png new file mode 100644 index 00000000000..83a2f6ffa61 Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0004-stream-lookup.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0005-filter-targets.png b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0005-filter-targets.png new file mode 100644 index 00000000000..56e11effa2a Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0005-filter-targets.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0005-switch-case.png b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0005-switch-case.png new file mode 100644 index 00000000000..970ad2fc1ba Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0005-switch-case.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0008-excel-stream-lookup.png b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0008-excel-stream-lookup.png new file mode 100644 index 00000000000..25aaf647728 Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0008-excel-stream-lookup.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0008-text-file-input-spark-readback.png b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0008-text-file-input-spark-readback.png new file mode 100644 index 00000000000..aaee3fe305e Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0008-text-file-input-spark-readback.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0099-complex.png b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0099-complex.png new file mode 100644 index 00000000000..4ece06c7573 Binary files /dev/null and b/docs/hop-user-manual/modules/ROOT/assets/images/pipeline/spark/spark-native-0099-complex.png differ diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/database.svg b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/database.svg new file mode 100644 index 00000000000..0ea6675f263 --- /dev/null +++ b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/database.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/spark-catalog.svg b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/spark-catalog.svg new file mode 100644 index 00000000000..dd89035553f --- /dev/null +++ b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/spark-catalog.svg @@ -0,0 +1,5 @@ + + + + diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/spark-lake-table-input.svg b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/spark-lake-table-input.svg new file mode 100644 index 00000000000..dd89035553f --- /dev/null +++ b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/spark-lake-table-input.svg @@ -0,0 +1,5 @@ + + + + diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/spark-lake-table-maintenance.svg b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/spark-lake-table-maintenance.svg new file mode 100644 index 00000000000..e3fb4293a69 --- /dev/null +++ b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/spark-lake-table-maintenance.svg @@ -0,0 +1,4 @@ + + + + diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/spark-lake-table-merge.svg b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/spark-lake-table-merge.svg new file mode 100644 index 00000000000..e3fb4293a69 --- /dev/null +++ b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/spark-lake-table-merge.svg @@ -0,0 +1,4 @@ + + + + diff --git a/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/spark-lake-table-output.svg b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/spark-lake-table-output.svg new file mode 100644 index 00000000000..e3fb4293a69 --- /dev/null +++ b/docs/hop-user-manual/modules/ROOT/assets/images/transforms/icons/spark-lake-table-output.svg @@ -0,0 +1,4 @@ + + + + diff --git a/docs/hop-user-manual/modules/ROOT/nav.adoc b/docs/hop-user-manual/modules/ROOT/nav.adoc index 7afae79f252..63fca1915d4 100644 --- a/docs/hop-user-manual/modules/ROOT/nav.adoc +++ b/docs/hop-user-manual/modules/ROOT/nav.adoc @@ -49,6 +49,11 @@ under the License. **** xref:pipeline/beam/beam-samples-spark.adoc[Apache Spark] **** xref:pipeline/beam/beam-samples-dataflow.adoc[Google Cloud Dataflow] *** xref:pipeline/beam/flink-k8s-operator-running-hop-pipeline.adoc[Running a Hop pipeline using the Flink Kubernetes Operator] +** xref:pipeline/spark/getting-started-with-native-spark.adoc[Getting started with the native Spark engine] +*** xref:pipeline/spark/paths-and-filesystems.adoc[Paths and file systems] +*** xref:pipeline/spark/cluster-mapping-walkthrough.adoc[Cluster mapping walk-through] +*** xref:pipeline/spark/lakehouse.adoc[Lakehouse tables (Delta / Iceberg)] +*** xref:pipeline/spark/databricks.adoc[Native Spark on Databricks] ** xref:pipeline/pipeline-metrics.adoc[Pipeline Metrics] ** xref:pipeline/pipeline-run-configurations/pipeline-run-configurations.adoc[Pipeline Run Configurations] *** xref:pipeline/pipeline-run-configurations/beam-dataflow-pipeline-engine.adoc[Beam Google DataFlow] @@ -57,6 +62,7 @@ under the License. *** xref:pipeline/pipeline-run-configurations/beam-spark-pipeline-engine.adoc[Beam Spark] *** xref:pipeline/pipeline-run-configurations/native-local-pipeline-engine.adoc[Native Local] *** xref:pipeline/pipeline-run-configurations/native-remote-pipeline-engine.adoc[Native Remote] +*** xref:pipeline/pipeline-run-configurations/native-spark-pipeline-engine.adoc[Native Spark] ** xref:pipeline/pipeline-unit-testing.adoc[Pipeline Unit Tests] ** xref:pipeline/metadata-injection.adoc[Metadata Injection] ** xref:pipeline/specify-copies.adoc[Specify copies] @@ -252,6 +258,10 @@ under the License. *** xref:pipeline/transforms/sort.adoc[Sort Rows] *** xref:pipeline/transforms/sortedmerge.adoc[Sorted Merge] *** xref:pipeline/transforms/sortedschemamerge.adoc[Sorted Schema Merge] +*** xref:pipeline/transforms/spark-lake-table-input.adoc[Spark Lake Table Input] +*** xref:pipeline/transforms/spark-lake-table-maintenance.adoc[Spark Lake Table Maintenance] +*** xref:pipeline/transforms/spark-lake-table-merge.adoc[Spark Lake Table Merge] +*** xref:pipeline/transforms/spark-lake-table-output.adoc[Spark Lake Table Output] *** xref:pipeline/transforms/splitfields.adoc[Split Fields] *** xref:pipeline/transforms/splitfieldtorows.adoc[Split fields to rows] *** xref:pipeline/transforms/splunkinput.adoc[Splunk Input] @@ -321,6 +331,8 @@ under the License. *** xref:workflow/actions/copyfiles.adoc[Copy Files] *** xref:workflow/actions/createfolder.adoc[Create a folder] *** xref:workflow/actions/createfile.adoc[Create file] +*** xref:workflow/actions/databricks-job-run.adoc[Databricks Job Run] +*** xref:workflow/actions/databricks-job-wait.adoc[Databricks Job Wait] *** xref:workflow/actions/pgpdecryptfiles.adoc[Decrypt files with PGP] *** xref:workflow/actions/deletefile.adoc[Delete file] *** xref:workflow/actions/deleteresultfilenames.adoc[Delete filenames from result] @@ -462,6 +474,7 @@ under the License. ** xref:metadata-types/rest-connection.adoc[REST Connection] ** xref:metadata-types/salesforce-connection.adoc[Salesforce Connection] ** xref:metadata-types/splunk-connection.adoc[Splunk Connection] +** xref:metadata-types/spark-catalog.adoc[Spark Catalog] ** xref:metadata-types/variable-resolver/index.adoc[Variable Resolver] *** xref:metadata-types/variable-resolver/azure-key-vault-variable-resolver.adoc[Azure Key Vault variable resolver] *** xref:metadata-types/variable-resolver/google-secret-manager-variable-resolver.adoc[Google Secret Manager variable resolver] diff --git a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/databricks-connection.adoc b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/databricks-connection.adoc new file mode 100644 index 00000000000..ba8aee6cce6 --- /dev/null +++ b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/databricks-connection.adoc @@ -0,0 +1,62 @@ +//// +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. +//// +:imagesdir: ../../assets/images/ +:page-pagination: +:description: Databricks Connection metadata for the Jobs / workspace REST API (PAT), used by Databricks Job Run actions. + += Databricks Connection + +== Description + +*Databricks Connection* stores workspace credentials for the **Databricks Jobs / workspace REST API**. +It is **not** the same as the xref:database/databases/databricks.adoc[Databricks JDBC] relational connection (SQL warehouse). + +Use this metadata type when orchestrating jobs from Hop (for example the upcoming **Databricks Job Run** workflow action) or when testing Jobs API access from the connection editor. + +== Options + +[options="header",cols="1,3"] +|=== +|Option |Description + +|Name +|Hop metadata name (how actions reference this connection). + +|Description +|Optional free-text description. + +|Workspace host +|Workspace URL host, e.g. `https://adb-1234567890123456.7.azuredatabricks.net`. The `https://` scheme is optional. + +|Personal access token +|Databricks PAT (stored as a password field). Prefer Hop variables / secret backends for production. Tokens are never written to Hop logs by the Jobs client. + +|API base path +|Jobs API prefix. Default: `/api/2.1`. +|=== + +== Test connection + +Use **Test connection** in the editor to call the workspace identity endpoint (SCIM *Me*) or fall back to listing jobs. A successful test shows the resolved user name or host. + +== Related + +* xref:pipeline/spark/databricks.adoc[Native Spark on Databricks] — Deploy & run, classic clusters, Volumes +* xref:workflow/actions/databricks-job-run.adoc[Databricks Job Run] — trigger and wait for Jobs API runs +* xref:database/databases/databricks.adoc[Databricks database (JDBC)] — SQL warehouse access +* xref:pipeline/spark/getting-started-with-native-spark.adoc[Native Spark pipeline engine] — fat jar / MainSpark on cluster +* xref:metadata-types/spark-catalog.adoc[Spark Catalog] — Unity Catalog templates for lakehouse TABLE mode diff --git a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/index.adoc b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/index.adoc index ca4b5ecd1ac..c4c716ac2d9 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/index.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/index.adoc @@ -41,6 +41,7 @@ By default, Hop contains the following metadata types: * xref:metadata-types/cassandra/cassandra-connection.adoc[Cassandra Connection]: Describes a connection to a Cassandra cluster * xref:metadata-types/data-set.adoc[Data Set]: This defines a data set, a static pre-defined collection of rows xref:metadata-types/data-stream/data-stream.adoc[Data Stream]: A Data Stream can be used for Inter Process Communication (IPC) or fast data serialization. +* xref:metadata-types/databricks-connection.adoc[Databricks Connection]: Workspace connection for the Databricks Jobs / REST API (PAT); not the JDBC SQL warehouse connection * xref:metadata-types/execution-data-profile.adoc[Execution Data Profile]: Collects and profiles data as it flows through a pipeline using configurable samplers for insight into value ranges, nulls, and row samples. * xref:metadata-types/execution-information-location.adoc[Execution Information Location]: Defines where and how Apache Hop stores execution metadata, supporting local files, remote servers, Neo4j, or Elastic for later inspection and analysis. * xref:metadata-types/google-storage-authentication.adoc[Google Storage Connection]: A Google Cloud Storage connection type. @@ -60,6 +61,7 @@ xref:metadata-types/data-stream/data-stream.adoc[Data Stream]: A Data Stream can * xref:metadata-types/rest-connection.adoc[REST Connection]: Describes all the metadata needed to connect to a REST api. * xref:metadata-types/salesforce-connection.adoc[Salesforce Connection]: Describes a reusable Salesforce connection that can be used across multiple Salesforce transforms * xref:metadata-types/splunk-connection.adoc[Splunk Connection]: Describes a Splunk connection +* xref:metadata-types/spark-catalog.adoc[Spark Catalog]: Spark SQL catalog configuration for Delta Lake / Apache Iceberg TABLE mode on the native Spark engine * xref:metadata-types/static-schema-definition.adoc[Static Schema Definition]: Defines a reusable data stream layout to ensure consistency across multiple pipelines and simplify schema management. * xref:metadata-types/variable-resolver/index.adoc[Variable Resolver]: Use plugins to resolve variable values with a pipeline, a key store, a vaults, or secret managers. * xref:hop-server/web-service.adoc[Web Service]: Allows to run a pipeline to generate output for a servlet on Hop Server diff --git a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/spark-catalog.adoc b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/spark-catalog.adoc new file mode 100644 index 00000000000..79fb3ab9167 --- /dev/null +++ b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/spark-catalog.adoc @@ -0,0 +1,150 @@ +//// +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. +//// +:imagesdir: ../../assets/images/ +:page-pagination: +:description: Spark Catalog metadata configures Spark SQL catalogs for Delta Lake and Apache Iceberg TABLE mode on the native Spark pipeline engine. + += Spark Catalog + +== Description + +*Spark Catalog* is a reusable Hop metadata type (category **Connections**) that describes a Spark SQL catalog for open table formats on the xref:pipeline/spark/getting-started-with-native-spark.adoc[native Spark pipeline engine]. + +Lake Table transforms in **TABLE** mode can reference a Spark Catalog by name. Hop expands the entry into `spark.sql.catalog..*` configuration when building or preparing the session (`LakeSessionPlan` / `SparkCatalogApplier`). + +PATH-mode Iceberg tables do **not** require this metadata type — the engine auto-registers a built-in Hadoop catalog named `hop_iceberg`. Use Spark Catalog for production Iceberg warehouses, REST catalogs, and other named catalogs. + +Connector packaging and session matrix: xref:pipeline/spark/lakehouse.adoc[Lakehouse tables on the native Spark engine]. + +== Load catalog template + +Use **Load catalog template** at the top of the form to fill fields from a named scenario. Placeholders (bucket, host, URI) must still be adjusted for your environment. Secrets are never stored in templates — put tokens in **Credential**. + +Every template writes a commented documentation link as the first line of **Extra conf**, for example: + +---- +# docs: https://iceberg.apache.org/docs/latest/spark-configuration/ +---- + +`#` lines are ignored when Hop builds the Spark session, so the link is guidance only. + +[options="header",cols="1,2,3"] +|=== +|Template |Type |What it sets + +|Iceberg Hadoop (local) +|`hadoop` +|Catalog `lake`, warehouse `file:///tmp/hop-warehouse` + +|Iceberg Hadoop (object store) +|`hadoop` +|Catalog `lake`, warehouse `s3a://bucket/warehouse`, S3 `io-impl` in conf extra + +|Iceberg REST +|`rest` +|Catalog `lake`, URI `https://catalog.example.com/v1` + +|Iceberg REST (authenticated) +|`rest` +|Same as REST; conf extra notes that **Credential** maps to catalog `.token` + +|Hive Metastore (advanced) +|`hive` +|Catalog `hive`, thrift URI placeholder (extra deps required) + +|AWS Glue (advanced) +|`glue` +|Catalog `glue`, S3 warehouse + IO placeholders (extra deps required) + +|Nessie (advanced) +|`custom` +|Catalog `nessie`, Nessie `catalog-impl` / URI / `ref` placeholders + +|Databricks Unity Catalog (advanced) +|`custom` +|Catalog `unity`, commented workspace URI/warehouse examples + +|Delta named catalog (advanced) +|`custom` +|`DeltaCatalog` implementation; prefer `spark_catalog` when coexisting with Iceberg +|=== + +The Hop metadata **Name** (how transforms select this entry) is not changed by a template — only catalog fields are. + +== Options + +[options="header",cols="1,3"] +|=== +|Option |Description + +|Name +|Hop metadata name (how transforms reference this entry). + +|Load catalog template +|Button that applies a named scenario (see above). Confirms before replacing non-empty fields. + +|Catalog name +|Spark SQL catalog name — first segment of `catalog.namespace.table` (e.g. `lake`). + +|Catalog type +|`hadoop` — Iceberg Hadoop warehouse (default). + +`rest` — Iceberg REST catalog. + +`custom` — supply implementation + conf extra. + +`hive` / `glue` — advanced; conf-extra oriented, not packaged by default. + +|Implementation +|Spark catalog class. Default for Iceberg is `org.apache.iceberg.spark.SparkCatalog`. + +|Warehouse +|Warehouse path for Hadoop (and similar) catalogs, e.g. `file:///data/warehouse` or an object-store URI. + +|URI +|Catalog service URI (REST and similar). + +|Credential +|Optional secret (password field). For REST catalogs, applied as `spark.sql.catalog..token` when set. + +|Conf extra +|Additional `key=value` lines (one per line; `#` comments and blank lines ignored). + +Short keys (e.g. `io-impl`) become `spark.sql.catalog..io-impl`. + +Keys that already start with `spark.` are applied as full Spark conf keys. +|=== + +== How it is applied + +For a catalog named `lake` of type `hadoop` with warehouse `file:///tmp/wh`, Hop effectively sets: + +---- +spark.sql.catalog.lake=org.apache.iceberg.spark.SparkCatalog +spark.sql.catalog.lake.type=hadoop +spark.sql.catalog.lake.warehouse=file:///tmp/wh +---- + +Lake Table transforms then use identifiers such as `lake.db.orders`. + +When both Delta and Iceberg are used in one session, keep **Delta** on `spark_catalog` (`DeltaCatalog`) and put Iceberg under a **different** catalog name (e.g. `lake` or `hop_iceberg`). + +== spark-submit + +Export project metadata JSON that includes every Spark Catalog entry referenced by the pipeline (and any used only via run configuration). Alternatively, pass equivalent `--conf spark.sql.catalog.…` flags on the submit command. Paths and warehouses must be reachable from the driver and executors. + +== Related pages + +* xref:pipeline/spark/lakehouse.adoc[Lakehouse tables on the native Spark engine] +* xref:pipeline/transforms/spark-lake-table-input.adoc[Spark Lake Table Input] +* xref:pipeline/transforms/spark-lake-table-output.adoc[Spark Lake Table Output] +* xref:pipeline/spark/getting-started-with-native-spark.adoc[Getting started with the native Spark pipeline engine] diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/pipeline-run-configurations/native-spark-pipeline-engine.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/pipeline-run-configurations/native-spark-pipeline-engine.adoc new file mode 100644 index 00000000000..fa00b1929a6 --- /dev/null +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/pipeline-run-configurations/native-spark-pipeline-engine.adoc @@ -0,0 +1,218 @@ +//// +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. +//// +[[NativeSparkPipelineEngine]] +:imagesdir: ../../../assets/images +:description: Apache Hop native Spark pipeline engine run configuration options (Spark 4.1.x without Beam). + += Native Spark Pipeline Engine + +== Overview + +The native Spark pipeline engine executes batch Hop pipelines on *Apache Spark 4.1.x* without Apache Beam. +It builds a Spark SQL Dataset graph from the pipeline and runs transforms via native Dataset operators or generic `mapPartitions` mini-pipelines. + +For a full introduction — concepts, `local[*]`, fat jar, `spark-submit`, Spark File I/O, `${Internal.Transform.ID}`, metrics, limitations, and integration-test examples — see +xref:pipeline/spark/getting-started-with-native-spark.adoc[Getting started with the native Spark pipeline engine]. + +Paths on *Spark File* and *Lake Table* transforms are **Spark/Hadoop URIs**, not Hop VFS schemes — see +xref:pipeline/spark/paths-and-filesystems.adoc[Paths and file systems on native Spark]. + +This page is *not* the same as xref:pipeline/pipeline-run-configurations/beam-spark-pipeline-engine.adoc[Beam Spark], which runs Hop over the Apache Beam Spark runner (Spark 3.5.x line). + +image::pipeline/spark/native-spark-pipeline-engine-run-configurtion-editor.png[Native Spark pipeline run configuration editor,width=70%] + +=== Requirements + +* Java 21 +* Spark 4.1.x (bundled under `plugins/engines/spark/lib` for `local[*]`) + +=== Options + +==== Load configuration template + +Use **Load configuration template** to fill the fields below from a common deployment style: + +* *Local execution* — `local[*]`, Spark UI off (embedded Spark in Hop) +* *Local (capped cores)* — `local[4]` +* *Spark standalone server* — `spark://host:7077` (replace host), sample driver/executor memory +* *spark-submit (cluster)* — master **empty** so `spark-submit --master` wins; use a `native-provided` fat jar +* *Databricks* — submit-oriented placeholders (master empty); set workspace-specific values yourself +* *YARN client* — master `yarn`, client deploy mode +* *Lakehouse local* — same as local; Delta/Iceberg connectors live on the plugin classpath + +Templates replace the current engine options after confirmation when the form already has custom values. The form fields update immediately after you apply a template. Edit values afterward as needed. + +For named Iceberg/Delta catalogs used by Lake Table transforms in **TABLE** mode, see xref:metadata-types/spark-catalog.adoc[Spark Catalog] (**Load catalog template**). + +[options="header",cols="1,3,1"] +|=== +|Option |Description |Default + +|Spark master +|Spark master URL: `local[*]`, `local[4]`, or `spark://host:7077`. Leave empty under `spark-submit` so the CLI master is used. +|`local[*]` + +|Application name +|Spark application name. +|`Apache Hop` + +|Fat jar file location +|Optional Hop fat jar staged for executors when Hop starts the Spark session. Leave empty for pure `spark-submit` with a native-provided fat jar. +| + +|Spark config (key=value lines) +|Additional SparkConf entries, one `key=value` per line. +| + +|Driver memory +|`spark.driver.memory` (e.g. `2g`). +| + +|Executor memory +|`spark.executor.memory`. +| + +|Executor cores +|`spark.executor.cores`. +| + +|Temp location +|Directory for temporary files. +|JVM temp dir + +|Plugins to stage +|Optional comma-separated plugin folders to stage on the cluster. +| + +|Path scheme map (`from=to` lines) +|Optional URI scheme rewrite for *Spark File* / *Lake PATH* only (e.g. `s3=s3a`, `minio=s3a`). Does not apply to classic Hop VFS transforms. Configure target FS (e.g. `spark.hadoop.fs.s3a.*`) separately. See xref:pipeline/spark/paths-and-filesystems.adoc[Paths and file systems]. +| + +|Generic transform run mode +|Default for Hop transforms that run as `mapPartitions` mini-pipelines (Workflow Executor, most classic transforms). `DISTRIBUTED` fans work out across Spark partitions/nodes. `DRIVER_ONLY` materializes the transform input on the Spark driver and runs the mini-pipeline once there, so nested workflows do not execute on multiple nodes. Override per transform with the *Spark Run Mode* context action on the canvas. +|`DISTRIBUTED` +|=== + +Also configure *Execution information location* and *Execution data profile* on the run configuration main tab when you want GUI metrics history and row samples. + +==== Generic transform run mode and per-transform override + +Most classic Hop transforms (including *Workflow Executor*, *Pipeline Executor*, and mappings) execute on Native Spark as generic `mapPartitions` work. When the input Dataset has many partitions, Spark can run that nested work on multiple executors at once. That is often desirable for pure row transforms, but nested workflows or other non-reentrant side effects may need a single place of execution. + +* **Run configuration default** — *Generic transform run mode*: `DISTRIBUTED` (default) or `DRIVER_ONLY`. +* **Per-transform override** — right-click a transform → *Spark Run Mode*: +** *Inherit* — use the run configuration default +** *Force distributed* — always use `mapPartitions` across partitions +** *Force Driver Only* — always run this transform's mini-pipeline on the Spark driver ++ +The override is stored on the transform (`attributes` group `spark` / key `run_mode`) and is saved with the pipeline. When a force override is set, a small badge is drawn at the **bottom-left** of the transform icon. + +image::pipeline/spark/native-spark-run-mode-force-driver.png[Transform with Force Driver Only Spark run-mode badge,width=70%] + +Native Dataset handlers (Spark File / Lake Table, Memory Group By, Sort, Unique, Merge Join) ignore this setting. + +[NOTE] +==== +`DRIVER_ONLY` collects that transform's input rows on the driver. Prefer `DISTRIBUTED` for high-volume data; use driver-only for nested execution control (for example a Workflow Executor over a modest set of control rows). +==== + +==== Nested Native Spark pipelines (orchestrator pattern) + +A parent Native Spark pipeline can start **child** Native Spark pipelines via **Pipeline Executor** (run configuration = Native Spark): + +* Put **Force Driver Only** (or run-config `DRIVER_ONLY`) on the Pipeline Executor so children start on the driver. +* Each child **reuses the parent `SparkSession`** (same Spark application) — it does **not** start a second `spark-submit`. +* Parameterize the child with control keys (country, month, partition) for paths and filters. +* Nested engines **must not stop** the shared session when they finish. + +Do **not** run nested Native Spark executors under **DISTRIBUTED** mapPartitions on workers (no safe nested SparkContext). For multi-application isolation (separate Spark apps per key), use an external job launcher instead. + +Demo: sample project `spark-demo` pipeline `pipelines/03-run-pipelines.hpl` (see xref:pipeline/spark/cluster-mapping-walkthrough.adoc[Cluster demo walk-through]). + +[[transforms-not-supported-on-native-spark]] +=== Transforms not supported on Native Spark + +Most classic Hop transforms run on Native Spark via native Dataset handlers or generic `mapPartitions`. +The list below is taken from each transform’s *Supported Engines* table (**Native Spark** = Not Supported). +When in doubt, open the transform page — that table is the source of truth. + +For broader design limits (copy/distribute, streaming, Spark SQL, MLlib), see +xref:pipeline/spark/getting-started-with-native-spark.adoc#limitations-and-design-rules[Getting started — Limitations and design rules]. + +==== Beam-only transforms + +Use a xref:pipeline/pipeline-run-configurations/beam-spark-pipeline-engine.adoc[Beam] engine instead. + +* xref:pipeline/transforms/beambigqueryinput.adoc[BigQuery Input] +* xref:pipeline/transforms/beambigqueryoutput.adoc[BigQuery Output] +* xref:pipeline/transforms/beambigtableinput.adoc[Beam Bigtable Input] +* xref:pipeline/transforms/beambigtableoutput.adoc[Beam Bigtable Output] +* xref:pipeline/transforms/beamfileinput.adoc[Beam File Input] +* xref:pipeline/transforms/beamfileoutput.adoc[Beam File Output] +* xref:pipeline/transforms/beamgcppublisher.adoc[Beam GCP Pub/Sub : Publish] +* xref:pipeline/transforms/beamgcpsubscriber.adoc[Beam GCP Pub/Sub : Subscribe] +* xref:pipeline/transforms/beamhivecataloginput.adoc[Beam Hive Catalog Input] +* xref:pipeline/transforms/beamkafkaconsume.adoc[Beam Kafka Consume] +* xref:pipeline/transforms/beamkafkaproduce.adoc[Beam Kafka Produce] +* xref:pipeline/transforms/beamkinesisconsume.adoc[Beam Kinesis Consume] +* xref:pipeline/transforms/beamkinesisproduce.adoc[Beam Kinesis Produce] +* xref:pipeline/transforms/beamtimestamp.adoc[Beam Timestamp] +* xref:pipeline/transforms/beamwindow.adoc[Beam Window] + +==== Barriers, stream ordering, and merge-style transforms + +These rely on waiting for other transforms, full-stream ordering, or multi-stream merge semantics that do not map cleanly to a Spark Dataset graph. + +* xref:pipeline/transforms/analyticquery.adoc[Analytic Query] +* xref:pipeline/transforms/append.adoc[Append Streams] +* xref:pipeline/transforms/blockingtransform.adoc[Blocking transform] +* xref:pipeline/transforms/blockuntiltransformsfinish.adoc[Blocking until transforms finish] +* xref:pipeline/transforms/detectemptystream.adoc[Detect Empty Stream] +* xref:pipeline/transforms/identifylastrow.adoc[Identify last row in a stream] +* xref:pipeline/transforms/mergerows.adoc[Merge rows (diff)] +* xref:pipeline/transforms/multimerge.adoc[Multiway Merge Join] +* xref:pipeline/transforms/sortedmerge.adoc[Sorted Merge] +* xref:pipeline/transforms/sortedschemamerge.adoc[Sorted Schema Merge] +* xref:pipeline/transforms/streamschemamerge.adoc[Stream Schema Merge] + +==== Sorted aggregation and hash-set unique + +* xref:pipeline/transforms/groupby.adoc[Group By] — use xref:pipeline/transforms/memgroupby.adoc[Memory Group By] (native Spark `groupBy`) instead +* xref:pipeline/transforms/uniquerowsbyhashset.adoc[Unique Rows (HashSet)] — use xref:pipeline/transforms/uniquerows.adoc[Unique Rows] where possible + +==== Workflow / pipeline result and control transforms + +Result-row and result-file hops, injectors, and metadata injection are not supported as Native Spark graph nodes. + +* xref:pipeline/transforms/copyrowstoresult.adoc[Copy rows to result] +* xref:pipeline/transforms/filesfromresult.adoc[Files from result] +* xref:pipeline/transforms/filestoresult.adoc[Files to result] +* xref:pipeline/transforms/getrowsfromresult.adoc[Get Rows from Result] +* xref:pipeline/transforms/injector.adoc[Injector] +* xref:pipeline/transforms/metainject.adoc[Metadata Injection] +* xref:pipeline/transforms/pipeline-data-probe.adoc[Pipeline Data Probe] +* xref:pipeline/transforms/repeatfields.adoc[Repeat Fields] + +==== Other + +* xref:pipeline/transforms/kafkaconsumer.adoc[Kafka Consumer] — no native Spark structured-streaming / Kafka Dataset I/O; use Beam Kafka transforms on a Beam engine for Kafka pipelines + +=== Related + +* xref:pipeline/spark/getting-started-with-native-spark.adoc[Getting started with the native Spark pipeline engine] +* xref:pipeline/spark/lakehouse.adoc[Lakehouse tables on the native Spark engine] (Delta Lake / Apache Iceberg) +* xref:pipeline/pipeline-run-configurations/beam-spark-pipeline-engine.adoc[Beam Spark pipeline engine] diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/pipeline-run-configurations/pipeline-run-configurations.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/pipeline-run-configurations/pipeline-run-configurations.adoc index a92a49d52f3..660797a34e9 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/pipeline-run-configurations/pipeline-run-configurations.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/pipeline-run-configurations/pipeline-run-configurations.adoc @@ -58,6 +58,7 @@ The available engine types are: * *xref:pipeline/pipeline-run-configurations/beam-spark-pipeline-engine.adoc[Beam Spark pipeline engine]*: this configuration runs pipelines on Apache Spark over Apache Beam * *xref:pipeline/pipeline-run-configurations/native-local-pipeline-engine.adoc[Hop local pipeline engine]*: this configuration runs pipelines locally in the native Hop engine * *xref:pipeline/pipeline-run-configurations/native-remote-pipeline-engine.adoc[Hop remote pipeline engine]*: this configuration runs pipelines in the native Hop engine on a remote machine +* *xref:pipeline/pipeline-run-configurations/native-spark-pipeline-engine.adoc[Native Spark pipeline engine]*: this configuration runs batch pipelines on Apache Spark 4.x *without* Beam (see xref:pipeline/spark/getting-started-with-native-spark.adoc[getting started]) * *xref:pipeline/pipeline-run-configurations/single-threaded-pipeline-engine.adoc[Hop local single threaded pipeline engine]*: this configuration runs pipelines locally in a single-threaded fashion |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/pipelines.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/pipelines.adoc index 0ce83d4c85e..3f692b967dd 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/pipelines.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/pipelines.adoc @@ -56,4 +56,6 @@ Pipelines are an extensive topic. Check the pages below to learn more about work * xref:pipeline/metadata-injection.adoc[Metadata Injection] * xref:pipeline/partitioning.adoc[Partitioning] * xref:pipeline/beam/getting-started-with-beam.adoc[Getting started with Apache Beam] +* xref:pipeline/spark/getting-started-with-native-spark.adoc[Getting started with the native Spark pipeline engine] +* xref:pipeline/spark/lakehouse.adoc[Lakehouse tables on the native Spark engine (Delta / Iceberg)] * xref:pipeline/transforms.adoc[Transforms] diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/cluster-mapping-walkthrough.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/cluster-mapping-walkthrough.adoc new file mode 100644 index 00000000000..71218233577 --- /dev/null +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/cluster-mapping-walkthrough.adoc @@ -0,0 +1,328 @@ +//// +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. +//// +[[NativeSparkClusterMappingWalkthrough]] +:imagesdir: ../../../assets/images +:description: Walk-through: run Native Spark pipelines (Simple Mapping and Workflow Executor) on a Docker Spark 4.1 cluster using a project package. + += Cluster demo walk-through (Native Spark) + +This walk-through shows that the Native Spark **project package** design works on a *real multi-node* Spark 4.1 cluster — not only on `local[*]`. + +You will: + +. Build a **`native-provided`** fat jar (cluster provides Spark). +. Export a **Native Spark project package** (definitions + metadata). +. Start a Docker Compose cluster (master + workers, shared data volume). +. `docker exec` **spark-submit** with `MainSpark --HopProjectPackage=…`. +. See a **Simple Mapping** child under `${PROJECT_HOME}` execute on workers via **SparkFiles**. +. Optionally run a **Workflow Executor** demo that creates per-country folders and instance marker files on the shared volume. +. Optionally run a **nested Native Spark** demo: Pipeline Executor on the driver starts a full child Spark pipeline per country (shared `SparkSession`). + +Sample project (in source tree): + +`plugins/engines/spark/src/samples/spark-demo` + +Compose file: + +`docker/integration-tests/integration-tests-spark-native-cluster.yaml` + +== What this proves + +[options="header",cols="1,2"] +|=== +|Claim |How the demo shows it + +|Nested mappings need files on every executor +|Child path is `${PROJECT_HOME}/pipelines/mappings/upper-name.hpl` — not embedded in the parent + +|Nested workflows need the same package +|Workflow Executor child is `${PROJECT_HOME}/workflows/create-country-folder.hwf` + +|Project package is the right unit +|Package zip is exported specifically for Native Spark (GUI or `hop-conf --export-spark-project`) + +|Multi-host shipping works +|Engine calls `SparkContext.addFile`; workers resolve with `SparkFiles.get` then extract + +|Data ≠ definitions +|Input/output and workflow side effects use **`HOP_DATA=file:///data/hop-data`** (shared volume), not package `PROJECT_HOME` +|=== + +Related design docs: xref:pipeline/spark/paths-and-filesystems.adoc[Paths and file systems], xref:pipeline/spark/getting-started-with-native-spark.adoc[Getting started with the native Spark engine]. + +== Prerequisites + +* Docker with Compose v2 +* Hop client **2.19+** with the native Spark plugin (from a built assembly or install) +* Enough disk to download Spark 4.1.x images (~1 GB first build) +* From a git checkout of Hop (paths below assume **repository root**) + +== 1. Prepare the fat jar and package on the host + +[source,bash] +---- +# Optional: point at your Hop install +export HOP_HOME=/path/to/hop # directory that contains hop-conf.sh + +./plugins/engines/spark/src/samples/spark-demo/scripts/prepare-dist.sh +---- + +By default this writes into **`/tmp/spark-demo-dist/`** (outside the git tree; override with `DIST_DIR`): + +* `hop-native-spark4-submit.jar` — fat jar with token **`native-provided`** (no embedded Spark) +* `spark-demo.zip` — Native Spark project package +* `cluster-env.json` — sets `HOP_DATA` for the cluster +* `data/customers-sample.csv` — mapping demo CSV +* `data/countries.csv` — workflow demo CSV (Genovia, Wakanda, Latveria) + +You do **not** need to register `spark-demo` in the system Hop configuration. +`prepare-dist.sh` exports with: + +[source,bash] +---- +hop-conf.sh --export-spark-project=/tmp/spark-demo-dist/spark-demo.zip \ + --export-spark-project-home=…/plugins/engines/spark/src/samples/spark-demo +---- + +[NOTE] +==== +Hop has two different project flags: + +* **`-j `** — *enable* a registered project for this command (sets `PROJECT_HOME`) +* **`-p` / `--project `** — only *names* a project for create/delete/list management + +`export-spark-project` needs a home folder via `--export-spark-project-home=…` or a project already enabled with **`-j`**. Using only `--project=` does not set `PROJECT_HOME`. +==== + +Alternatively in the GUI: + +* *Tools* → *Generate a Hop fat jar* with native-provided packaging, and +* *Tools* → *Export project package for Native Spark…* after opening the sample project. + +== 2. Start the Spark 4.1 cluster + +[source,bash] +---- +docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml \ + up -d --build --scale spark-worker=2 +---- + +Compose mounts: + +* `${HOP_DIST_DIR:-/tmp/spark-demo-dist}` → `/opt/hop-dist` (fat jar + project package; master) +* `${HOP_DATA_HOST_DIR:-/tmp/spark-demo-dist/hop-data}` → `/data/hop-data` (shared data plane; **master and workers**, host-visible) + +If you used a non-default `DIST_DIR` in `prepare-dist.sh`, set both `HOP_DIST_DIR` and `HOP_DATA_HOST_DIR=${HOP_DIST_DIR}/hop-data` for compose. + +* Master UI: http://localhost:8080 (two workers should register). +* Images use the existing `docker/integration-tests/spark` Dockerfiles with **`SPARK_VERSION=4.1.2`** (override with env if needed). +* After a run you can inspect **on the host** without `docker exec`, for example: + +[source,bash] +---- +ls -la /tmp/spark-demo-dist/hop-data/out +ls -la /tmp/spark-demo-dist/hop-data/executions # if an execution information location is configured +---- + +== 3. Submit the demo job + +[source,bash] +---- +docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml \ + exec spark /opt/hop-samples/spark-demo/scripts/spark-submit-demo.sh +---- + +What the script does: + +. Seeds `/data/hop-data/customers-sample.csv` and `/data/hop-data/countries.csv` on the shared volume. +. Runs `spark-submit --master spark://spark:7077 --deploy-mode client --class org.apache.hop.spark.run.MainSpark …` +. Passes `--HopProjectPackage=/opt/hop-dist/spark-demo.zip` and + `--HopPipelinePath=pipelines/01-enrich-with-mapping.hpl` (default). +. Loads `cluster-env.json` so `HOP_DATA=file:///data/hop-data`. + +In the logs you should see package materialization / distribution and a normal pipeline finish. + +== 4. Verify mapping output + +On the host (bind mount): + +[source,bash] +---- +ls -la /tmp/spark-demo-dist/hop-data/out/enriched +head -n 20 /tmp/spark-demo-dist/hop-data/out/enriched/* +---- + +Or inside the container (`/data/hop-data` is the same directory): + +[source,bash] +---- +docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml \ + exec spark ls -la /data/hop-data/out/enriched + +docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml \ + exec spark sh -c 'head -n 20 /data/hop-data/out/enriched/*' +---- + +Expect a header plus rows with an uppercase `displayName` column (from the mapping child). + +== 5. Workflow Executor demo (optional) + +Submit the second sample pipeline (same package and cluster): + +[source,bash] +---- +docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml \ + exec spark /opt/hop-samples/spark-demo/scripts/spark-submit-workflows-demo.sh +---- + +Inspect country folders and marker files (host path): + +[source,bash] +---- +find /tmp/spark-demo-dist/hop-data/out/countries -type f | sort +---- + +Expect: + +[source] +---- +/tmp/spark-demo-dist/hop-data/out/countries/Genovia/.txt +/tmp/spark-demo-dist/hop-data/out/countries/Wakanda/.txt +/tmp/spark-demo-dist/hop-data/out/countries/Latveria/.txt +---- + +* Three country directories mean Workflow Executor ran the child workflow **once per input row**. +* Marker basenames come from `${Internal.Pipeline.ID}` (parent context via `inherit_all_vars`). +** Same basename in every folder → one parent transform/pipeline instance handled all rows (typical single partition or **DRIVER_ONLY**). +** Different basenames → multiple Spark partition instances of Workflow Executor (**DISTRIBUTED** fan-out). + +The Native Spark run configuration **Generic transform run mode** (`DISTRIBUTED` / `DRIVER_ONLY`) and the transform context action *Spark Run Mode* control whether Workflow Executor runs as distributed `mapPartitions` or on the driver. See xref:pipeline/pipeline-run-configurations/native-spark-pipeline-engine.adoc[Native Spark pipeline engine]. + +A 3-row CSV may still use a single partition under `DISTRIBUTED`; the folders still prove package path resolution and shared-volume side effects. + +== 6. Nested Native Spark pipelines (optional) + +For **heavy** per-key work (Dataset I/O, shuffles), use **Pipeline Executor** with a **Native Spark** child run configuration and **Force Driver Only** on the executor transform — not a new `spark-submit` per key. + +[source,bash] +---- +docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml \ + exec spark /opt/hop-samples/spark-demo/scripts/spark-submit-pipelines-demo.sh +---- + +[source,bash] +---- +find /tmp/spark-demo-dist/hop-data/out/by-country -type f | sort +---- + +Expect `out/by-country/{Genovia,Wakanda,Latveria}/…` under the host hop-data dir, and logs: +`Nested Native Spark pipeline reusing parent SparkSession`. + +Child definition: `${PROJECT_HOME}/pipelines/nested/enrich-by-country.hpl` (in the project package). Control plane stays on the driver; each child builds a Dataset graph on the **same** Spark session. + +== Pipeline shapes (why they matter) + +=== Mapping + +[source] +---- +Spark File Input (${HOP_DATA}/customers-sample.csv) + │ + ▼ +Simple Mapping (filename = ${PROJECT_HOME}/pipelines/mappings/upper-name.hpl) + │ ↑ only works if the package was extracted on this executor + ▼ +Spark File Output (${HOP_DATA}/out/enriched) +---- + +The child mapping concatenates first + last name and uppercases the result. If the package were missing on a worker, the job would fail opening the child `.hpl` — that is the gap this design closes. + +=== Workflow Executor + +[source] +---- +Spark File Input (${HOP_DATA}/countries.csv) + │ + ▼ +Workflow Executor (filename = ${PROJECT_HOME}/workflows/create-country-folder.hwf) + parameter COUNTRY_NAME ← country_name + → Create Folder ${HOP_DATA}/out/countries/${COUNTRY_NAME} + → Create File …/${Internal.Pipeline.ID}.txt +---- + +Side-effect paths use **`HOP_DATA`** (shared volume). The workflow definition uses **`PROJECT_HOME`** (package). + +=== Nested Native Spark (orchestrator) + +[source] +---- +Spark File Input (${HOP_DATA}/countries.csv) + │ + ▼ +Pipeline Executor Force Driver Only; runConfiguration = spark-cluster (Native Spark) + │ filename = ${PROJECT_HOME}/pipelines/nested/enrich-by-country.hpl + │ parameter COUNTRY_NAME ← country_name + ▼ +Child SparkPipelineEngine (reuses parent SparkSession) + Spark File Input → Spark File Output (${HOP_DATA}/out/by-country/${COUNTRY_NAME}) +---- + +== Tear down + +[source,bash] +---- +docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml down +---- + +Host data under `/tmp/spark-demo-dist/hop-data` is left in place so you can keep inspecting outputs and execution-info JSON. Delete that directory manually if you want a clean slate. + +== Troubleshooting + +[options="header",cols="1,2"] +|=== +|Symptom |What to check + +|Fat jar / package not found in container +|`HOP_DIST_DIR` must point at the host folder mounted as `/opt/hop-dist`. Re-run `prepare-dist.sh`. + +|Workers cannot open mapping file +|Confirm logs mention distributing the package / `SparkFiles`. Scale workers ≥ 1 and ensure submit used `--HopProjectPackage`. + +|Spark File Input path not found +|`HOP_DATA` must be the **shared** volume path (`file:///data/hop-data`). Do not point Dataset paths at package `PROJECT_HOME`. + +|ClassNotFoundException / wrong Spark +|Fat jar must be **`native-provided`**. Cluster image must be Spark **4.1.x** + **Java 21**. Do not mix Beam Spark 3 fat jars. + +|Driver cannot reverse-connect workers +|Submit script sets `spark.driver.host=spark` (master hostname on the compose network). Adjust if you change service names. + +|Spark download slow / fails on build +|Dockerfiles fall back to archive.apache.org. Set `SPARK_BASE_URL` or pre-build images once. +|=== + +== Follow-ups (not required for this walk-through) + +* MinIO + `s3a://` data plane (needs `hadoop-aws` on the Spark classpath). +* HDFS instead of a bind mount. +* CI job that runs prepare + compose + submit non-interactively. + +== Related pages + +* xref:pipeline/spark/getting-started-with-native-spark.adoc[Getting started with the native Spark engine] +* xref:pipeline/spark/paths-and-filesystems.adoc[Paths and file systems on native Spark] +* xref:pipeline/pipeline-run-configurations/native-spark-pipeline-engine.adoc[Native Spark pipeline engine] diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/databricks.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/databricks.adoc new file mode 100644 index 00000000000..c62ef8dabce --- /dev/null +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/databricks.adoc @@ -0,0 +1,544 @@ +//// +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. +//// +[[NativeSparkOnDatabricks]] +:imagesdir: ../../../assets/images +:description: Run Apache Hop native Spark (MainSpark JAR) jobs on Databricks — workspace tiers, classic clusters, UC Volumes, compute modes, and Deploy & run lessons learned. +:toc: +:toclevels: 3 + += Native Spark on Databricks + +This page captures practical lessons for running **Hop native Spark** pipelines on a **Databricks** workspace via the xref:workflow/actions/databricks-job-run.adoc[Databricks Job Run] **Deploy & run** path (`MainSpark` JAR task). + +It is not a full Databricks admin guide. It focuses on what usually goes wrong when connecting Hop to Free / trial / serverless-oriented workspaces versus a **classic-cluster-capable** (e.g. Premium) workspace suitable for Spark 4.1 and modern Java. + +For general native Spark concepts (engine, fat jar, `local[*]` first), start with +xref:pipeline/spark/getting-started-with-native-spark.adoc[Getting started with the native Spark engine]. + +== Sample project shape + +A minimal Hop project for this path typically includes: + +* A small pipeline to run on the cluster (e.g. generate rows → random UUID → dummy) +* A workflow with **Databricks Job Run** (Deploy & run) and optional **Databricks Job Wait** +* A `work/` folder with the **native-provided** fat jar +* Metadata: Databricks Connection, Native Spark pipeline run configuration + +.Sample `hello-databricks.hpl` in the project explorer (Generate rows → UUID → Dummy) +image::pipeline/spark/databricks-pipeline-hello-databricks.png[Hello Databricks sample pipeline in Hop GUI: Generate rows, Random value (uuid), Dummy,width=85%] + +== What Deploy & run does + +When mode is **Deploy & run**, the Job Run action: + +. Uploads artifacts to a workspace path (UC Volume or classic DBFS). Names are chosen so concurrent deploys and library refreshes stay correct: + * fat jar → **content-addressed** `hop-native-.jar` (first 12 hex chars of the local jar’s SHA-256) plus a matching `.sha256` sidecar — skip upload when that remote path already matches size + sidecar (see <> and <>) + * pipeline → `pipeline-{stem}.hpl` (simple mode; may be omitted when a package-relative path is used) + * exported Hop metadata → `metadata-{stem}.json` (simple mode; embedded in the package when package mode is on) + * optional Spark project package → `hop-spark-package-{stem}.zip` (nested Simple Mapping / Pipeline Executor) + * optional environment config → `env-config-{stem}.json` (MainSpark `--HopConfigFile`) + * `{stem}` = sanitized pipeline file basename without extension (e.g. `hello-mapping-databricks`) +. Creates or updates a Databricks **Jobs** definition with a `spark_jar_task`: + * main class: `org.apache.hop.spark.run.MainSpark` + * parameters: classic positional `pipeline`, `metadata`, `runConfig` — or named `--Hop*` args when a project package and/or env file is present +. Calls `jobs/run-now` and optionally waits for completion (or fire-and-forget + + xref:workflow/actions/databricks-job-wait.adoc[Databricks Job Wait]). + +Credentials come from xref:metadata-types/databricks-connection.adoc[Databricks Connection] +metadata (workspace host + personal access token). That is **not** the +xref:database/databases/databricks.adoc[Databricks JDBC] SQL warehouse connection. + +.Deploy & run dialog: connection, fat jar / pipeline paths, UC Volume upload base, classic `new_cluster` (e.g. DBR `18.2.x-scala2.13`, AWS `i3.xlarge`) +image::pipeline/spark/databricks-workflow-deploy-to-databricks.png[Databricks Job Run dialog: Deploy and run with fat jar, pipeline, Volume base path, and new_cluster settings,width=90%] + +== Workspace and tier: classic clusters vs serverless + +Hop’s Deploy & run path is built for a **classic compute** story: either an **existing +all-purpose cluster** or a classic **job cluster** (`new_cluster` in the Jobs API). + +[options="header",cols="1,2,2"] +|=== +|Workspace style |Typical compute |Hop Deploy & run JAR jobs + +|Free / some trial / serverless-only +|No (or very limited) user-created classic clusters; serverless Jobs only +|*Not* a good fit for full MainSpark JAR deploy today. Serverless has different job shape (environments / `environment_key` / `java_dependencies`), runtime limits (often older Java / constrained Spark), and no dynamic classic cluster create. + +|Premium (or equivalent) with **classic clusters** enabled +|All-purpose clusters and/or classic job clusters (e.g. AWS `m5.*`, DBR with Spark 4.x) +|**Supported path.** Create or attach a classic cluster that can run your Spark / Java line, then use existing cluster id or `new_cluster`. +|=== + +=== Lessons learned + +* **Free tier / “cannot create a cluster”** — many Free or UI-first workspaces never expose a classic cluster id. Deploy & run then has nothing valid to put in `existing_cluster_id`. Putting the literal string `new_cluster` into that field without a nested object produces errors such as _Cluster new_cluster does not exist_ (the API treated it as a cluster id). +* **Serverless-only error** — `Only serverless compute is supported in the workspace` means classic `new_cluster` / existing all-purpose clusters are blocked. Serverless needs job-level `environments` and task `environment_key`, and **must not** send task-level `libraries` the same way classic jobs do. +* **Spark 4.1 / Java 21 style workloads** — for Hop’s **native** Spark 4.x engine and a fat jar built for modern runtimes, provision a workspace that **allows classic clusters** (e.g. AWS Premium workspace) and a DBR / job cluster configuration aligned with your Spark and JVM expectations. Do not assume Free/serverless will match local `native-provided` fat-jar testing. +* **New workspace for classic** — if the current workspace is permanently serverless-oriented, creating a **new** classic-capable workspace (cloud account + tier that allows classic compute) is often simpler than fighting Free-tier product limits. +* [[lesson-fat-jar-library-uri]]**Fat jar library URI must change when jar bytes change (Dedicated / long-lived cluster “caching crapout”)** — overwriting a **fixed** Volume path such as `/Volumes/…/hop-native.jar` (even with Files API `?overwrite=true` and a fresh `.sha256` sidecar) is **not** enough for Databricks to always run the new classes. A long-lived **Dedicated** (or other all-purpose) cluster often keeps **previously installed job libraries** on the driver/executor classpath (old URI still wins even after a successful Volume overwrite). Symptoms: + **Volume object is minutes old and matches local `sha256sum`**, but MainSpark driver log still shows **old behavior** (e.g. missing `>>>>>> Spark project package distribution build: pkg-dist-…` right after `Initializing Hop`, or obsolete package-distribution log strings such as `cluster-shared … (no SparkFiles)`). Hop Deploy & run therefore uploads a **content-addressed** library path `hop-native-.jar` and points the job’s `libraries[].jar` at that URI so each content change is a **new library identity**. **Still required after the first switch** (or if old `hop-native.jar` remains installed on the cluster): **restart the target Dedicated cluster** so accumulated libraries are cleared, then re-run. Verify the driver log prints the expected `pkg-dist-…` fingerprint before debugging package/mapping logic. + +== Connection and secrets + +* Create a **Databricks Connection** (Jobs / PAT), not only a JDBC entry. +* Prefer Hop **variables** for host and token (project or lifecycle environment JSON). +* Password-style fields and variable values may be stored as `Encrypted …`. The Jobs client resolves variables and then calls `Encr.decryptPasswordOptionallyEncrypted` so both plain and encrypted PAT values work. +* **Test connection** in the metadata editor uses the same variable space as Hop GUI (`MetadataManager` / `HopGui` variables). After changing the plugin, rebuild the **client assembly** so the running `plugins/tech/databricks` jar matches your source (evaluate-in-IDE vs stale plugin jar is a common false lead). + +Example environment variables: + +---- +DATABRICKS_HOST=https://dbc-….cloud.databricks.com +DATABRICKS_TOKEN=Encrypted … +---- + +Connection fields: + +---- +host = ${DATABRICKS_HOST} +token = ${DATABRICKS_TOKEN} +---- + +CLI (with projects/environments registered in Hop config): + +[source,bash] +---- +cd $HOP_HOME # e.g. assemblies/client/target/hop +export HOP_CONFIG_FOLDER=/path/to/your/hop-config-dir +./hop run -j my-project -e my-databricks-env \ + -f main-deploy-to-databricks.hwf -r local -l BASIC +---- + +[[artifact-storage]] +== Artifact storage: UC Volumes vs DBFS + +Modern workspaces often have **public DBFS root disabled**. Uploads to +`dbfs:/FileStore/…` then fail with: + +---- +PERMISSION_DENIED: Public DBFS root is disabled +---- + +=== Prefer Unity Catalog Volumes + +. Create a volume (e.g. catalog `apache-hop`, schema `default`, volume `jars`). +. Set **Upload base directory** on Job Run Deploy & run to something like: + +---- +/Volumes/apache-hop/default/jars +---- + +Artifacts land as (names depend on jar content and entry pipeline stem): + +---- +/Volumes/apache-hop/default/jars/hop-native-2f80e51734a5.jar +/Volumes/apache-hop/default/jars/hop-native-2f80e51734a5.jar.sha256 +/Volumes/apache-hop/default/jars/pipeline-hello-databricks.hpl +/Volumes/apache-hop/default/jars/metadata-hello-databricks.json +/Volumes/apache-hop/default/jars/hop-spark-package-hello-databricks.zip +---- + +The `.sha256` sidecar next to the content-addressed jar lets later deploys **skip re-uploading** the same bytes (see <>). The content-addressed **filename** is what forces Databricks to treat a new jar as a new job library (see <>). + +.UC Volume after deploy: fat jar, SHA-256 sidecar, exported metadata, and pipeline +image::pipeline/spark/databricks-workspace-catalog-jars-volume.png[Databricks Catalog Explorer volume jars showing hop-native jar, checksum sidecar, metadata, and pipeline artifacts,width=80%] + +=== Which upload API Hop uses + +[options="header",cols="1,2,2"] +|=== +|Path prefix |API |Notes + +|`/Volumes/…` or `/Workspace/…` +|**Files API** — `PUT /api/2.0/fs/files{path}?overwrite=true` with `Content-Type: application/octet-stream` +|Required for UC Volumes. Single streaming PUT (up to ~5 GiB). Parent folders/volume must exist and be writable by the PAT. + +|Classic `dbfs:/FileStore/…` (or other non-Volume paths) +|**Legacy DBFS** — `dbfs/create` → `add-block` → `close` +|Only where public DBFS root is still enabled. Not interchangeable with Volumes. +|=== + +If logs still show `POST /api/2.0/dbfs/create` for a `/Volumes/…` path, the running plugin jar is almost certainly **older than the Files API fix** — rebuild/copy `hop-tech-databricks` into the client `plugins/tech/databricks` folder and restart Hop. + +Job library and MainSpark parameter paths for volumes stay as absolute `/Volumes/…` (no `dbfs:` scheme). + +== Compute modes (Deploy & run) + +The **Cluster / compute** field accepts one of: + +[options="header",cols="1,2,2"] +|=== +|Value |Jobs API shape |When to use + +|**Existing cluster id** +|Task: `existing_cluster_id` + task `libraries` (jar) +|You already started a classic all-purpose cluster. Paste its cluster id. + +|Literal **`new_cluster`** +|Task: nested `new_cluster` object (`spark_version`, `node_type_id`, `num_workers`, …) + task `libraries` +|Classic **job cluster**: Databricks creates a cluster for the run and tears it down afterward (similar idea to ephemeral Dataflow workers). **Not** the string `"existing_cluster_id": "new_cluster"`. + +|Literal **`serverless`** +|Job: `environments` with `environment_key` + `spec.client` + `java_dependencies`; task: `environment_key` only (no cluster fields, no task `libraries`) +|Serverless-only workspaces. JAR-on-serverless is a different product surface (runtime limits, library rules). Prefer classic for Hop native Spark 4.x / Java 21 style deploy until you explicitly target serverless. +|=== + +=== Classic job cluster (`new_cluster`) defaults + +When the field is `new_cluster`, Hop can build: + +[source,json] +---- +"new_cluster": { + "spark_version": "18.2.x-scala2.13", + "node_type_id": "i3.xlarge", + "num_workers": 1 +} +---- + +* **`spark_version`** — Databricks Runtime string. Example: `18.2.x-scala2.13` (DBR line shipping Spark 4.1.x / Scala 2.13). Align with the runtime you validated for the fat jar. +* **`node_type_id`** — cloud-specific. On **AWS**, prefer **`i3.xlarge`** (local NVMe SSD; no extra EBS block required). Avoid bare `m5.xlarge` without EBS (see <>). Azure example: `Standard_DS3_v2`. Wrong type → create-job 400. +* **`num_workers`** — worker count (string fields support variables). +* Optional **full new_cluster JSON** overrides the structured fields when you need more knobs (Spark conf, single-node, EBS volumes, etc.). + +[[spark-conf-cores]] +==== Match Spark cores and parallelism to the node + +Hop’s simple `new_cluster` defaults only set `spark_version`, `node_type_id`, and `num_workers`. Without explicit Spark conf, DBR may under-use the instance CPUs and multi-million-row Hop pipelines can crawl even when the cluster looks “idle.” + +**Tune `spark_conf` so cores and default parallelism match the machine you bought.** Example for **one worker `i3.xlarge` (4 vCPUs)** with the driver and executor sharing that capacity: + +[source,json] +---- +"new_cluster": { + "spark_version": "18.2.x-scala2.13", + "node_type_id": "i3.xlarge", + "num_workers": 1, + "spark_conf": { + "spark.executor.cores": "3", + "spark.driver.cores": "1", + "spark.default.parallelism": "12", + "spark.sql.shuffle.partitions": "12" + } +} +---- + +| Setting | Example (1× `i3.xlarge`, 4 cores) | Intent | +| --- | --- | --- | +| `spark.executor.cores` | `3` | Executor threads on the worker | +| `spark.driver.cores` | `1` | Driver share of the same node (3+1 = 4 cores available) | +| `spark.default.parallelism` | `12` | Default RDD partitions (often a few × total cores) | +| `spark.sql.shuffle.partitions` | `12` | Shuffle partitions; keep in the same ballpark | + +Put this under **optional new_cluster JSON** on the Job Run **Compute** tab (cluster field = `new_cluster`). Scale the numbers when you change node type or worker count (more workers → more total cores → raise parallelism/partitions). + +**Observed impact (Hello-style multi-million-row Native Spark pipeline on classic job compute):** pipeline **execution** dropped from on the order of **~10 minutes** to about **~41 seconds** after applying the conf above — cold start / Pending time is unchanged (see <>). + +[[job-cluster-cold-start]] +==== Job cluster cold start is slow + +With **`new_cluster`**, Databricks provisions a fresh classic job cluster for the run. That is **not** instant: cloud VMs, DBR image pull, and Spark/JVM startup often take **several minutes**. A small sample such as *Hello Databricks* can sit in **Pending / Initializing** for **~8 minutes or more** before the pipeline itself starts — especially on the first run of a node type/runtime in the account. + +.Jobs UI: *Hello Databricks* run stuck in Initializing while the classic job cluster starts (normal; often many minutes) +image::pipeline/spark/databricks-jobs-pipelines-hello-databricks-initializing.png[Databricks Jobs run for Hello Databricks showing pipeline Initializing while the job cluster starts,width=90%] + +Once the cluster is up, the same run moves to **Running** and a tiny pipeline usually finishes quickly: + +.Jobs UI: *Hello Databricks* pipeline Running after the job cluster is ready +image::pipeline/spark/databricks-jobs-pipelines-hello-databricks-running.png[Databricks Jobs run for Hello Databricks showing pipeline Running,width=90%] + +When the fat jar includes the TrapExit-safe `MainSpark` exit path, the same run ends as **Succeeded**: + +.Jobs UI: *Hello Databricks* pipeline Succeeded (after cluster start + pipeline run) +image::pipeline/spark/databricks-jobs-pipelines-hello-databricks-succeeded.png[Databricks Jobs run for Hello Databricks showing pipeline Succeeded,width=90%] + +Even a trivial *Hello Databricks* job still pays **Spark / JVM baseline** on the driver container: metrics often show on the order of **~4 GB** of container memory used (not “a few MB for three transforms”). That is expected overhead for DBR + Spark + loading the fat jar — size your node type for the **platform floor**, not only for row volume. + +.Metrics: *Hello Databricks* container memory used ~4 GB vs a much higher limit on `i3.xlarge` +image::pipeline/spark/databricks-jobs-pipelines-hello-databricks-memory-usage-4gb.png[Databricks container memory usage chart for Hello Databricks showing about 4 GB used,width=90%] + +Plan timeouts accordingly: + +* On **Databricks Job Run** (wait mode) or **Databricks Job Wait**, set **timeout** high enough for **jar upload (first time) + cluster start + pipeline run** — e.g. **3600s** (1 hour), not a few minutes. +* Prefer fire-and-forget + Job Wait if you do not want the workflow action to hold a long poll in one step. +* **Existing all-purpose cluster** (already Running) avoids most of this wait; **`new_cluster`** always pays the cold-start cost each run. + +=== Existing classic cluster + +. In Databricks UI: **Compute** → create/start a classic all-purpose cluster on the new workspace. +. **Access mode must be Dedicated (single user)** — not **Standard** (shared) and not Serverless. Standard/Serverless use **Spark Connect**, which does **not** expose `SparkContext`. Hop Native Spark needs a real context for RDDs, accumulators, and mapPartitions. +. Wait until **Running**. +. Copy **Cluster ID**. +. Set **Cluster / compute** to that id (not `new_cluster` / not `serverless`). +. Leave job id empty on first create; after success, store `${DatabricksJobId}` if you enable **Update existing job**. + +If you see: + +---- +[UNSUPPORTED_CONNECT_FEATURE.SESSION_SPARK_CONTEXT] Feature is not supported in Spark Connect: +SparkContext is not available on Standard access mode or Serverless compute. +Switch to Dedicated access mode to access it. +---- + +…the cluster is the wrong access mode. Either switch the all-purpose cluster to **Dedicated**, or use Deploy & run with **`new_cluster`** (classic job cluster), which provides a full SparkContext. + +== FAQ + +[[faq-ebs-m5]] +=== Create job fails: “At least one EBS volume must be attached … node type m5.xlarge” + +**Symptom** + +---- +Databricks API HTTP 400 for POST /api/2.1/jobs/create +INVALID_PARAMETER_VALUE +At least one EBS volume must be attached for clusters created with node type m5.xlarge. +---- + +**Cause** + +On AWS, **`m5.xlarge`** (and many non-`i3` instance types) do not ship local instance store disks that Databricks can use as worker/driver local storage for a classic job cluster. Databricks then requires an **EBS volume** attachment in the `new_cluster` definition. Hop’s simple defaults only set `spark_version`, `node_type_id`, and `num_workers` — no EBS block — so `m5.xlarge` fails at `jobs/create`. + +**Fix (simplest, recommended on AWS)** + +Switch **`node_type_id`** to **`i3.xlarge`** (or another **`i3.*`** size that fits your budget). + +* `i3` instances include **local NVMe SSDs**, so Databricks does **not** require extra EBS configuration for a basic job cluster. +* `i3.xlarge` is a commonly used worker type for Databricks jobs. + +In the Job Run dialog (Deploy & run, cluster field = `new_cluster`): + +* Set **new_cluster node_type_id** to `i3.xlarge` + +Or in optional **new_cluster JSON**: + +[source,json] +---- +{ + "spark_version": "18.2.x-scala2.13", + "node_type_id": "i3.xlarge", + "num_workers": 1 +} +---- + +**Alternative** + +Keep `m5.xlarge` (or similar) and supply a full `new_cluster` JSON that includes the required **EBS volume** settings for your account/cloud (see Databricks Jobs API / cluster create docs for `aws_attributes` / disk specs). Prefer `i3.xlarge` unless you have a reason to stay on EBS-backed types. + +=== Other common create/deploy errors + +[options="header",cols="1,2"] +|=== +|Message (summary) |What to do + +|Public DBFS root is disabled / FileStore access denied +|Use a UC Volume path (`/Volumes/…`), not `dbfs:/FileStore/…`. See <> above. + +|`/Volumes/… cannot be opened for writing` with `dbfs/create` +|Volume path must use the **Files API**, not legacy DBFS. Rebuild/update the Databricks plugin; upload base should start with `/Volumes/`. + +|Cluster `new_cluster` does not exist +|Do not put the word `new_cluster` as an existing cluster id without the nested object. Use cluster field **`new_cluster`** so Hop emits a real `new_cluster` block. + +|Only serverless compute is supported +|Workspace is serverless-only. Use classic-capable workspace/tier for MainSpark JAR, or `serverless` compute mode (different job shape; not the default for Hop native Spark 4.x / Java 21 style deploy). +|=== + +[[faq-exit-marks-failed]] +=== Pipeline log says success but Databricks marks the job **Failed** + +**Symptom** + +Driver stdout looks healthy, for example: + +---- +Spark pipeline finished, result row count=1000 +>>>>>> Execution finished... +---- + +…then Databricks Jobs still shows the run as **Failed**. You may also see: + +---- +Error running native Spark pipeline: Program attempted to exit with code 0 +com.databricks.backend.daemon.driver.ExitSecurityException: Program attempted to exit with code 0 + at com.databricks.backend.daemon.driver.TrapExitSecurityManager.checkExit(...) + at java.base/java.lang.System.exit(...) + at org.apache.hop.spark.run.MainSpark.main(...) +---- + +Logs may mention cleaning a REPL wrapper. A line such as `Hop configuration file not found … /databricks/driver/config/hop-config.json` is **benign** (Hop skips serializing config when that path is missing). + +**Cause** + +Databricks installs **`TrapExitSecurityManager`**: any `System.exit` (including **code 0**) throws **`ExitSecurityException`** instead of ending the process. Older `MainSpark` called `System.exit(0)` after a successful pipeline; the security manager blocked it, and the generic catch block mis-reported that as *Error running native Spark pipeline*, so Jobs marked the run **Failed** even though Spark finished (`result row count=1000`). Same class of issue as Databricks KB *“Job fails, but Apache Spark tasks finish”*. + +**Fix** + +* Use a fat jar where `MainSpark` **returns from `main` without `System.exit(0)`** on Databricks (env / `SPARK_HOME` under `/databricks` / TrapExit security manager), and treats a trapped exit-0 as success if exit is still attempted. +* Rebuild **native-provided** fat jar, re-upload (or refresh via size/checksum skip), and re-run. +* On plain `spark-submit` (non-Databricks), success still uses `System.exit(0)` so non-daemon Spark threads do not hang the JVM. + +**Healthy driver log (success)** — Jobs UI should show **Succeeded**: + +---- +>>>>>> Initializing Hop +Hop configuration file not found, not serializing: /databricks/driver/config/hop-config.json +Argument : Pipeline path (.hpl) : /Volumes/apache-hop/default/jars/pipeline.hpl +Argument : Metadata export (.json) : /Volumes/apache-hop/default/jars/metadata.json +Argument : Pipeline run configuration : databricks-native +>>>>>> Loading pipeline metadata +>>>>>> Validating native Spark engine plugin in fat jar... +>>>>>> SPARK_HOME=/databricks/spark +>>>>>> Pipeline execution starting (native Spark)... +2026/07/18 13:06:41 - pipeline - Prepared native Spark pipeline engine with run configuration 'databricks-native' (master=) +2026/07/18 13:06:41 - pipeline - Reusing active SparkSession (version=4.1.0, spark-submit or existing driver context) +2026/07/18 13:06:53 - General - Handled transform '5M rows' with generic Spark DISTRIBUTED (plugin id=RowGenerator) +2026/07/18 13:06:54 - General - Handled transform 'uuid' with generic Spark DISTRIBUTED (plugin id=RandomValue) +2026/07/18 13:06:54 - General - Handled transform 'results' with generic Spark DISTRIBUTED (plugin id=Dummy) +2026/07/18 13:16:43 - pipeline - Spark pipeline finished, result row count=5000000 +>>>>>> Execution finished... +>>>>>> Skipping System.exit(0) (Databricks / TrapExit security manager) +---- + +Notes on that log: + +* `Hop configuration file not found … hop-config.json` — **benign**. +* `Reusing active SparkSession` — expected on Databricks (do not `spark.stop()` / do not force a second context). +* `Handled transform …` lines are **graph build** (seconds); wall time until `result row count=…` is the real work. +* Final line **`Skipping System.exit(0) (Databricks / TrapExit security manager)`** is the success marker for the exit fix above; the Jobs UI should show **Succeeded** (see the screenshot under <>). +* Example timing: without core/parallelism conf, **5 000 000** rows can take on the order of **~10 minutes** on classic job compute; with <> (e.g. 3+1 on `i3.xlarge`), the same style workload has been seen around **~40 seconds** of *execution* time. A pure Spark SQL `range` job would still be faster; Hop generic transforms are not that path (see also <>). + +[[faq-low-cpu-rowgen]] +=== Low cluster CPU / slow Row Generator (millions of rows) + +**Symptom** + +Databricks metrics show only **~5–7%** cluster CPU while a multi-million-row pipeline runs for many minutes. You expected “a few seconds” for 5M rows. + +**Cause** + +Source transforms such as **Generate rows** are wired as **one empty driver row → `repartition(1)` → one `mapPartitions` task**. Downstream generic transforms (e.g. Random value, Dummy) stay on that **single partition** unless something shuffles. Each partition runs a **single-threaded Hop mini-pipeline** (not Catalyst `range`), and the engine materializes with `Dataset.count()` at the end. Cluster-wide CPU stays low because **most cores never get a task**. + +**What to expect** + +* Spark UI: source transforms such as Generate rows still often start as **1 task** (see engine design). +* Still set <> so DBR uses the CPUs you pay for — under-configured clusters show low CPU and multi-minute runs for millions of rows even after the cluster is “Running”. +* Wall clock for generic Hop transforms remains higher than pure Spark SQL; local clusters show the same architectural limits. + +**When you need real scale** + +Prefer partitioned sources (files, lake tables, or a multi-partition generator), keep high-volume work in native Spark I/O where possible, and treat generic Hop `mapPartitions` as **compatibility**, not as a bulk SQL engine. + +[[faq-grpc-context]] +=== Job fails at init: `NoClassDefFoundError: io/grpc/Context` + +**Symptom** + +---- +Error running native Spark pipeline: +java.lang.NoClassDefFoundError: io/grpc/Context + at … GoogleCredentials.getApplicationDefault … + at org.apache.hop.vfs.gs.GoogleStorageFileProvider … + at org.apache.hop.core.HopEnvironment.init … + at org.apache.hop.spark.run.MainSpark.main … +---- + +**Cause** + +During `HopEnvironment.init`, Hop registers VFS plugins. The **Google Storage** VFS provider used to call **Application Default Credentials** immediately. That path probes the GCE metadata service and needs **`io.grpc.Context`**. A **native-provided** fat jar typically includes Beam’s *vendored* gRPC (`org.apache.beam.vendor.grpc…`) but **not** plain `io.grpc.*`. On **Databricks AWS** (not GCP) ADC is unnecessary for a simple Hello pipeline, but the missing class still aborted Hop startup. + +**Fix** + +* Use a Hop build where Google Storage VFS **does not fail Hop init** when ADC/gRPC is unavailable (credentials loaded only when a key is configured, and linkage errors are swallowed for the default provider). +* Rebuild the fat jar from that install and re-upload (or let skip-upload refresh the sidecar after you replace the jar). +* If you actually need `gs://` on the cluster, either ship a service account key config or ensure full Google auth/gRPC libraries are on the driver classpath (not only Beam-vendored gRPC). + +== Fat jar and pipeline run configuration + +. Build a **native-provided** fat jar (Spark provided by the cluster, not bundled): + +[source,bash] +---- +./hop-conf.sh \ + --generate-fat-jar=/path/to/hop-native-spark-submit.jar \ + --spark-client-version=native-provided +---- + +. Create a **Native Spark** pipeline run configuration in project metadata (name is exported into `metadata.json`). Deploy & run passes that name as the third MainSpark argument. +. Point Deploy & run at: + * fat jar path (variables OK, e.g. `${PROJECT_HOME}/work/….jar`) + * pipeline `.hpl` + * that run configuration name + * upload base under a Volume + * classic compute (existing id or `new_cluster`) + +First jar upload is slow (hundreds of MB). Classic **`new_cluster`** cold start is also slow (often **~8+ minutes** of Pending/Initializing before a tiny pipeline runs — see above). Timeouts on the action should allow for upload **plus** cluster start **plus** the pipeline (e.g. **3600s**). + +[[faq-skip-jar-upload]] +=== Fat jar re-upload every run is slow — can Hop skip it? + +**Yes (content-addressed path + size + checksum sidecar).** Deploy & run computes the local fat jar’s SHA-256 and targets: + +---- +{upload-base}/hop-native-.jar +{upload-base}/hop-native-.jar.sha256 +---- + +. Probe that **content-addressed** remote path for **file size** (Files API metadata / DBFS get-status). +. If size matches, download the tiny sidecar and compare to the local **SHA-256**. +. If both match → log *Skipping fat jar upload* and leave the remote jar as-is (same library URI as last time). +. If the local jar content changed → the remote **filename** changes → upload a new object and point the job library at the new URI (see <>). + +Pipeline (`.hpl`) and metadata JSON are still uploaded each run when used (they are small). Project package zips are per-pipeline stem. + +**Manual check** (without Hop): + +[source,bash] +---- +ls -l work/hop-native-spark4-submit.jar +sha256sum work/hop-native-spark4-submit.jar +# First 12 hex chars of that sum → hop-native-<12hex>.jar on the Volume + +# Remote size (example Files API HEAD) +curl -sS -I -H "Authorization: Bearer $DATABRICKS_TOKEN" \ + "https://$DATABRICKS_HOST/api/2.0/fs/files/Volumes/.../hop-native-<12hex>.jar" +---- + +Databricks does **not** expose a standard content MD5/SHA for volume files comparable to S3 ETag, so Hop stores its own `.sha256` sidecar next to the jar. The **filename prefix** is the operational identity for job libraries; the sidecar is for skip-upload efficiency. + +== End-to-end checklist + +* [ ] Workspace allows **classic** compute (Premium or equivalent); not Free/serverless-only for JAR deploy +* [ ] Databricks Connection + variables for host/token; Test connection succeeds +* [ ] UC Volume exists and PAT can write; upload base is `/Volumes/…` (not disabled FileStore) +* [ ] Client assembly includes current `hop-tech-databricks` plugin (Files API + compute modes) +* [ ] Fat jar `native-provided`; Native Spark run config name matches deploy field +* [ ] After a fat jar rebuild: deploy log shows content-addressed `hop-native-.jar` (or restart Dedicated cluster if still on a fixed library path); driver log shows expected `pkg-dist-…` / MainSpark fingerprint when debugging library freshness +* [ ] Cluster / compute = existing classic id **or** `new_cluster` with valid `node_type_id` / `spark_version` +* [ ] Job id empty on first create; wait mode and timeout sized for upload + **job cluster cold start** (often 8+ minutes) + pipeline run +* [ ] After success: note Job ID, Run ID, run page URL variables; open run in Databricks UI + +== Related + +* xref:workflow/actions/databricks-job-run.adoc[Databricks Job Run] +* xref:workflow/actions/databricks-job-wait.adoc[Databricks Job Wait] +* xref:metadata-types/databricks-connection.adoc[Databricks Connection] +* xref:pipeline/spark/getting-started-with-native-spark.adoc[Getting started with the native Spark engine] +* xref:pipeline/pipeline-run-configurations/native-spark-pipeline-engine.adoc[Native Spark run configuration] +* xref:pipeline/spark/paths-and-filesystems.adoc[Paths and file systems] +* xref:database/databases/databricks.adoc[Databricks JDBC] (SQL warehouse — separate use case) +* xref:metadata-types/spark-catalog.adoc[Spark Catalog] (Unity Catalog templates for lakehouse TABLE mode) diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/getting-started-with-native-spark.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/getting-started-with-native-spark.adoc new file mode 100644 index 00000000000..d8bfda450bb --- /dev/null +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/getting-started-with-native-spark.adoc @@ -0,0 +1,706 @@ +//// +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. +//// +[[GettingStartedWithNativeSpark]] +:imagesdir: ../../../assets/images +:description: Getting started with the native Apache Spark pipeline engine in Apache Hop (Spark 4.x, without Apache Beam). + +:toc: +:toclevels: 3 + += Getting started with the native Spark pipeline engine + +This guide is for data engineers who already know the basics of Hop pipelines (transforms, hops, run configurations, metadata) and want to run *batch* pipelines on https://spark.apache.org[Apache Spark] using Hop’s *native* Spark engine. + +It walks you from “what is this?” through a `local[*]` first run, cluster `spark-submit`, Spark-oriented I/O, metrics, limitations, and concrete examples from the `integration-tests/spark-native` project. + +For **Delta Lake** and **Apache Iceberg** (ACID tables, catalogs, time travel, MERGE, maintenance), see the dedicated xref:pipeline/spark/lakehouse.adoc[lakehouse guide] after you have a working native Spark run configuration. + +Before putting object-store paths in *Spark File* or *Lake Table* transforms, read +xref:pipeline/spark/paths-and-filesystems.adoc[Paths and file systems on native Spark] — Hop VFS (`s3://`, named MinIO) and Spark/Hadoop URIs (`s3a://`, …) are different dialects. + +For **Simple Mapping** / **Pipeline Executor** child pipelines on `spark-submit`, export a +xref:pipeline/spark/paths-and-filesystems.adoc#spark-project-package[Spark project package] +(`hop-conf -j … --export-spark-project=…`) and pass `--HopProjectPackage=…` to `MainSpark`. + +Hands-on multi-node demo (Docker Compose + sample project `spark-demo` — Simple Mapping and Workflow Executor): +xref:pipeline/spark/cluster-mapping-walkthrough.adoc[Cluster demo walk-through]. + +To deploy **MainSpark** JAR jobs to a **Databricks** workspace (classic clusters, UC Volumes, Job Run Deploy & run): +xref:pipeline/spark/databricks.adoc[Native Spark on Databricks]. + +== What is the native Spark pipeline engine? + +=== In one sentence + +The native Spark pipeline engine takes the *metadata* of a Hop pipeline and executes it as a Spark SQL `Dataset` graph on *Apache Spark 4.1.x* — without translating the pipeline through Apache Beam. + +=== How that differs from Beam Spark + +Apache Hop already had a way to run pipelines on Spark: the xref:pipeline/pipeline-run-configurations/beam-spark-pipeline-engine.adoc[Beam Spark pipeline engine]. That path converts your Hop graph into an https://beam.apache.org[Apache Beam] pipeline, then uses Beam’s Spark *runner* (Spark *3.5.x* line). + +The *native* engine skips Beam entirely: + +* Hop’s `SparkPipelineEngine` builds a logical plan of Spark Datasets on the driver. +* Each Hop transform becomes either a *native handler* (Spark APIs such as `groupBy`, `join`, `read` / `write`) or a *generic* `mapPartitions` stage that runs a small single-threaded Hop mini-pipeline once per Spark partition. +* You use Hop’s *Spark File Input* / *Spark File Output* transforms for bulk Dataset I/O (not Beam File Input/Output). + +[options="header",cols="1,2,2"] +|=== +|Topic |Native Spark engine |Beam Spark engine + +|Purpose +|Batch Hop pipelines on Spark Datasets / mapPartitions +|Beam pipelines on Spark (batch *and* streaming) + +|Spark line +|https://spark.apache.org/docs/4.1.2/[4.1.x] (Scala 2.13) +|3.5.x (Scala 2.12; see Beam support matrix) + +|Programming model +|Spark SQL `Dataset` + Hop mini-pipelines +|Apache Beam PCollections + runners + +|Streaming / windows +|Out of scope — use Beam +|Supported via Beam + +|Preferred bulk I/O +|*Spark File Input* / *Spark File Output* +|*Beam File Input* / *Beam File Output* + +|Side data (lookups) +|Info streams (broadcast), e.g. Stream Lookup +|Beam side inputs + +|Conditional fan-out +|Target streams (Filter Rows, Switch/Case) +|Beam multi-output tags + +|Engine id (plugin) +|`SparkPipelineEngine` — “Native Spark pipeline engine” +|Beam Spark run configuration +|=== + +If you need streaming, windowing, or Beam-only connectors, stay on the Beam engines. If you want a straightforward batch path on modern Spark 4 with Dataset-oriented I/O, use native Spark. + +=== How a Hop pipeline becomes a Spark job + +. You design a normal Hop pipeline (`.hpl`) and select a *Native Spark* pipeline run configuration. +. On prepare, the engine walks the *enabled* transform graph in order. +. For each transform it either: +** applies a *native handler* that rewrites the hop as Spark Dataset operations, or +** wraps the transform in `mapPartitions` so each Spark partition runs that transform in a tiny local Hop pipeline. +. File outputs and other Spark *actions* materialise the graph (read → transform → write). +. Transform metrics are collected via Spark accumulators and shown in Hop like any other engine. + +You still design pipelines *visually* in Hop. Spark’s role is the distributed execution engine and the Dataset runtime under the hood. + +=== Version support matrix + +[NOTE] +==== +*Java:* Hop requires *Java 21* for the GUI, `hop-run`, and the fat jar executed on the Spark driver/executors. Your Spark cluster must also run a JVM that can load that bytecode (Java 21). + +*Spark:* The native engine is built and tested against *Spark 4.1.x* (currently *4.1.2* in the Hop source tree, Scala *2.13*). For `local[*]` runs, Spark libraries are packaged under `plugins/engines/spark/lib`, so you do *not* need a separate Spark install on your laptop. +==== + +[options="header",cols="1,1,2,2"] +|=== +|Hop version |Native Spark engine |Target Spark |Notes + +|2.19.0+ +|Included (`plugins/engines/spark`) +|4.1.x (Scala 2.13) +|Independent of Beam’s Spark 3.5.x client packs + +|Earlier Hop versions +|— +|— +|Native Spark engine not available; use Beam Spark if needed +|=== + +Do *not* mix this engine with Beam’s Spark 3.5 fat-jar packs or an accidental `SPARK_HOME` that points at Spark 3.x when you intend to use native Spark 4.1. + +== Prerequisites + +* Apache Hop *2.19+* with the *engines-spark* plugin (standard Hop client / assembly includes it). +* *Java 21*. +* Familiarity with Hop pipelines, metadata, and run configurations. +* For cluster submit: +** A *Spark 4.1.x* distribution (`SPARK_HOME` pinned to that tree). +** Network access to the master and workers. +** Paths for data and metadata that workers can reach (HDFS, object store, shared FS). +* Optional but recommended: an *execution information location* and *execution data profile* for history, metrics, and row samples in the GUI. + +== How to use it + +=== Create a `local[*]` pipeline run configuration + +This is the fastest way to learn: Spark runs *inside* the Hop process using all local cores (`local[*]`), with no cluster and no fat jar. + +TIP: On the Native Spark engine options tab, **Load configuration template** can pre-fill common setups (*Local execution*, *Spark standalone server*, *spark-submit*, *Databricks*, *YARN*, …). See xref:pipeline/pipeline-run-configurations/native-spark-pipeline-engine.adoc[Native Spark pipeline engine]. + +TIP: For Iceberg **TABLE** mode, create a xref:metadata-types/spark-catalog.adoc[Spark Catalog] entry and use **Load catalog template** (Hadoop local/object store, REST, Hive, Glue, Nessie, …). Each preset starts **Extra conf** with a commented `# docs:` link to vendor documentation. + +. Open *Metadata* → *Pipeline run configuration* → *New*. +. Give it a clear name, for example `spark-local`. +. Set *Engine type* to *Native Spark pipeline engine*. +. On the engine tab, set at least: + +[options="header",cols="1,3"] +|=== +|Option |Suggested first value + +|Spark master +|`local[*]` (or `local[4]` to cap cores) + +|Application name +|`Apache Hop` (or your project name) + +|Fat jar file location +|Leave *empty* for pure `local[*]` + +|Spark config +|Optional; one `key=value` per line (advanced tuning) + +|Driver / executor memory & cores +|Optional; leave empty for local defaults + +|Temp location +|Optional; defaults to the JVM temp directory +|=== + +. On the main run-configuration tab, optionally attach: +** *Execution information location* — where Hop stores run history +** *Execution data profile* — which row samples to capture +. Save the configuration. + +image::pipeline/spark/native-spark-pipeline-engine-run-configurtion-editor.png[Native Spark pipeline run configuration editor,width=80%] + +Full option reference: xref:pipeline/pipeline-run-configurations/native-spark-pipeline-engine.adoc[Native Spark pipeline engine]. + +=== First pipeline on `local[*]` + +. Create a small batch pipeline, for example: +** *Spark File Input* — CSV path under your project, header and separator, field list +** *Filter Rows* or *Select Values* — any familiar transform +** *Spark File Output* — path, format (`csv` or `parquet`), save mode *Overwrite* +. Run the pipeline and choose your `spark-local` run configuration. +. When it finishes, check: +** Transform metrics on the pipeline graph (input / output / written / errors) +** Execution Information perspective if you configured a location + +You do not need `spark-submit` or a fat jar for this path. Hop starts an embedded Spark session and loads Spark from `plugins/engines/spark/lib`. + +=== Run from the command line (local) + +Same run configuration, no GUI: + +[source,bash] +---- +./hop-run.sh \ + --project=your-project \ + --runconfig=spark-local \ + --file=/path/to/your-pipeline.hpl +---- + +=== Build a fat jar for cluster submit + +On a real cluster you must *not* embed a second copy of Spark inside the application jar. The cluster already provides Spark 4.1. Embedding Spark leads to dual classpaths and failures on Java 21. + +Use the special fat-jar token *`native-provided`*: Hop and plugins go into the jar; *Spark stays out*. + +==== From Hop GUI + +. *Tools* → *Generate a Hop fat jar* (when prompted for Spark client version for native submit, use the *native-provided* style packaging for cluster submit — or use the CLI below for an explicit flag). +. Save the jar *outside* the project (or gitignore it). Fat jars are large. + +==== From the command line (recommended, explicit) + +[source,bash] +---- +# From your Hop installation directory +./hop-conf.sh \ + --generate-fat-jar=/tmp/hop-native-spark4-submit.jar \ + --spark-client-version=native-provided +---- + +[options="header",cols="1,2,2"] +|=== +|Token |When to use |Spark runtime + +|`native-provided` +|`spark-submit` on a cluster +|Cluster provides Spark 4.1.x (*do not* embed) + +|`native` +|Rare: fat jar that *includes* Spark 4 from `plugins/engines/spark/lib` (not for normal cluster submit) +|Embedded Spark 4 +|=== + +=== Export project metadata + +Cluster runs need your Hop metadata (run configurations, execution locations, connections, etc.) as a single JSON file. + +* GUI: *Tools* → *Export metadata to JSON* +* CLI: + +[source,bash] +---- +./hop-conf.sh \ + --project=your-project \ + --export-metadata=/tmp/hop-metadata.json +---- + +Keep this export out of git if it is a point-in-time dump of secrets or large metadata; your project `metadata/` folder is already the source of truth. + +=== Detailed `spark-submit` instructions + +. Install or unpack *Spark 4.1.x* (Hadoop 3 build is typical). +. *Pin `SPARK_HOME`* to that directory. If `SPARK_HOME` still points at an older Spark (for example 3.3), even a `spark-submit` binary from a 4.1 tree can re-exec the wrong `spark-class` and load the wrong version. +. Build the *native-provided* fat jar and export metadata (previous sections). +. Ensure the pipeline run configuration used on the cluster: +** Engine = *Native Spark pipeline engine* (not Beam Spark) +** *Master* blank (so `spark-submit --master` wins) *or* set to `spark://host:7077` +** *Fat jar* field empty (the submit jar already carries Hop) +. Submit: + +[source,bash] +---- +export SPARK_HOME=/path/to/spark-4.1.x-bin-hadoop3 + +"${SPARK_HOME}/bin/spark-submit" \ + --master spark://your-master:7077 \ + --deploy-mode client \ + --class org.apache.hop.spark.run.MainSpark \ + /tmp/hop-native-spark4-submit.jar \ + /path/to/pipeline.hpl \ + /tmp/hop-metadata.json \ + YourNativeSparkRunConfig \ + /path/to/cluster-env-config.json +---- + +*Main class:* `org.apache.hop.spark.run.MainSpark` + +*Positional arguments:* + +. Path to the pipeline (`.hpl`) — must be readable on the *driver* +. Path to metadata JSON — readable on the driver +. Name of the native Spark pipeline run configuration (as in metadata) +. Optional: *environment configuration file* (JSON) — extra variables for the run (see below) + +*Named arguments* (equivalent): + +[source,bash] +---- +"${SPARK_HOME}/bin/spark-submit" \ + --master spark://your-master:7077 \ + --class org.apache.hop.spark.run.MainSpark \ + /tmp/hop-native-spark4-submit.jar \ + --HopPipelinePath=/path/to/pipeline.hpl \ + --HopMetadataPath=/tmp/hop-metadata.json \ + --HopRunConfigurationName=YourNativeSparkRunConfig \ + --HopConfigFile=/path/to/cluster-env-config.json +---- + +==== Environment configuration file (4th argument) + +On a laptop, Hop projects usually set `PROJECT_HOME` to a local folder. In a *clustered* `spark-submit` job that path often has no meaning: drivers and executors do not share your desktop filesystem, and paths in pipelines are typically `${PROJECT_HOME}/…` or other project variables. + +Pass an optional **environment configuration file** as the 4th positional argument (or `--HopConfigFile=…`). It uses the same JSON format as a Hop environment / `hop-config` variables file. `MainSpark` loads every described variable into the pipeline variable space before execution. + +Example `cluster-env-config.json`: + +[source,json] +---- +{ + "variables": [ + { + "name": "PROJECT_HOME", + "value": "s3a://hop-project/", + "description": "Project root on object storage for the cluster run (Spark/Hadoop S3A URI, not Hop VFS s3://)" + }, + { + "name": "OUTPUT_ROOT", + "value": "s3a://hop-project/output/", + "description": "Shared output prefix visible to all executors" + } + ] +} +---- + +Use this for any value that must differ between local development and the cluster: connection endpoints, bucket prefixes, `${PROJECT_HOME}`, credentials placeholders you resolve elsewhere, and so on. + +[IMPORTANT] +==== +* The config file path must be readable on the *driver* (local path on the submit machine, or a URI Hop VFS can open there). +* Values such as `s3a://hop-project/` only help if workers can actually read/write that location (correct Spark/Hadoop **S3A** configuration, IAM roles, etc.). Prefer **`s3a://`** for Spark Dataset I/O — Hop VFS uses `s3://` for a *different* stack. See xref:pipeline/spark/paths-and-filesystems.adoc[Paths and file systems on native Spark]. +* Variables in this file override defaults for *this* submit only; they do not rewrite your project’s GUI environment on disk. +==== + +*Data paths* in the pipeline (files, tables, object stores) must be reachable from *workers*, not only from your laptop. Prefer HDFS, S3/GS/ABFS, or a shared mount — and set `PROJECT_HOME` (and similar) in the environment config so `${PROJECT_HOME}/…` resolves correctly on the cluster. + +*Resource flags* (optional examples): + +[source,bash] +---- +"${SPARK_HOME}/bin/spark-submit" \ + --master spark://your-master:7077 \ + --executor-memory 4g \ + --executor-cores 2 \ + --driver-memory 2g \ + --class org.apache.hop.spark.run.MainSpark \ + /tmp/hop-native-spark4-submit.jar \ + /path/to/pipeline.hpl \ + /tmp/hop-metadata.json \ + YourNativeSparkRunConfig \ + /path/to/cluster-env-config.json +---- + +You can also put memory/core settings in the run configuration’s engine options; CLI flags are useful for per-job overrides. + +[#_building_spark_oriented_pipelines] +== Building Spark-oriented pipelines + +=== Prefer Spark File Input and Spark File Output + +For large or production Dataset I/O, use the dedicated transforms in the *Big Data* category: + +* *Spark File Input* — `spark.read.format(...).load` +* *Spark File Output* — `df.write.format(...).save` + +Typical first pipeline shape: + +image::pipeline/spark/spark-native-0001-input-output.png[Spark File Input to Spark File Output sample,width=70%] + +==== Spark File Input + +Common options: + +[options="header",cols="1,3"] +|=== +|Option |Notes + +|Spark path (Hadoop URI) +|File, directory, or glob visible to Spark (local path on `local[*]`; cluster URI such as `hdfs://…` or `s3a://…` on submit). **Not** a Hop VFS path — see xref:pipeline/spark/paths-and-filesystems.adoc[Paths and file systems]. + +|Format +|`csv`, `parquet`, `json`, `orc`, `text` + +|Header / separator / quote +|CSV and text + +|Field list +|Preferred for stable schemas. With a header, columns are selected and cast *by name*. Without a header, Spark names columns `_c0`, `_c1`, … and Hop maps them *by position* to your field list. + +|Infer schema +|Optional; explicit fields are safer for production +|=== + +==== Spark File Output + +[options="header",cols="1,3"] +|=== +|Option |Notes + +|Spark path (Hadoop URI) +|Output directory/prefix Spark should write to (same Spark/Hadoop dialect as input; not Hop VFS) + +|Format +|`csv`, `parquet`, `json`, `orc`, `text` + +|Save mode +|Overwrite, Append, Ignore, ErrorIfExists + +|Header +|CSV/text + +|Coalesce +|e.g. `1` for a single part file (costs a shuffle) + +|Partition by +|Optional partition column list for directory partitioning +|=== + +Writes run as Spark *actions* when the engine materialises the graph. Prefer *Spark File Output* for large partitioned writes to object stores or HDFS. + +=== Open table formats (Delta Lake / Apache Iceberg) + +Classic *Spark File* transforms cover `csv` / `parquet` / `json` / `orc` / `text` only. For ACID lake tables use the dedicated **Lake Table** transforms (Big Data category): + +* xref:pipeline/transforms/spark-lake-table-input.adoc[Spark Lake Table Input] / xref:pipeline/transforms/spark-lake-table-output.adoc[Spark Lake Table Output] +* xref:pipeline/transforms/spark-lake-table-merge.adoc[Spark Lake Table Merge] / xref:pipeline/transforms/spark-lake-table-maintenance.adoc[Spark Lake Table Maintenance] +* xref:metadata-types/spark-catalog.adoc[Spark Catalog] metadata for Iceberg TABLE mode + +Connector JARs **ship in the default Hop assembly** under `plugins/engines/spark/lib/{delta,iceberg}` (and ride a *native-provided* fat jar for cluster submit). Full session conf, PATH vs TABLE, and samples: xref:pipeline/spark/lakehouse.adoc[Lakehouse tables on the native Spark engine]. + +=== Classic Hop I/O and `${Internal.Transform.ID}` + +You can still use classic transforms such as *Text File Input*, *Text File Output*, *Get File Names*, or *Microsoft Excel Input* on the generic mapPartitions path (useful for POCs and small lookups). Those plugins are staged for the Spark engine via `plugins/engines/spark/dependencies.xml`. + +*Sources with no previous hop* (for example Get File Names, or Excel/Text Input reading a fixed file list) run on a *single* partition so the same small file is not listed or opened once per executor — the same idea as Beam’s single-thread pattern for non-Beam inputs. + +==== Unique filenames per partition + +Each Spark partition runs its own copy of a classic writer. If every copy uses the same path, files overwrite each other. + +*Recommended:* put `${Internal.Transform.ID}` in the filename template, for example: + +[source,text] +---- +${PROJECT_HOME}/out/export_${Internal.Transform.ID}.csv +---- + +On the native Spark engine, that variable is set to a *readable* value: + +[source,text] +---- +{transform-name}-{partition-id} +---- + +`${Internal.Transform.CopyNr}` and `${Internal.Transform.BundleNr}` are set to the Spark partition id as well. + +*Also:* the engine enables Beam-parity parallel-file context so *Text File Output* can auto-append `__` to the built filename (same idea as Beam’s `beamContext` suffix). Relying on `${Internal.Transform.ID}` in the path remains the clearest approach for operators. + +[NOTE] +==== +For large distributed writes, prefer *Spark File Output* (Spark manages part files). Classic Text File Output is ideal for small POC side-loads and local/shared paths where unique names per partition are enough. +==== + +==== Accept filenames from a previous transform + +*Get File Names* → *Text File Input* (accept filenames from field) works on native Spark: filename rows are the main Dataset into Text File Input; the mini-pipeline rebinds the accept source to the partition injector and preloads filename rows before the file reader drains them. + +Example shape from the integration tests: + +image::pipeline/spark/spark-native-0008-text-file-input-spark-readback.png[Get File Names to Text File Input to Spark File Output,width=70%] + +== Metrics and execution information + +=== Live metrics in the pipeline editor + +While a native Spark pipeline runs (and when it finishes), Hop shows transform-level metrics on the graph — lines read/written/input/output, errors, and running/finished state — similar to the local engine. Values are aggregated on the *driver* from Spark accumulators. + +image::pipeline/spark/native-spark-provides-runtime-transform-metrics-and-state.png[Runtime transform metrics and state on a native Spark pipeline,width=90%] + +=== Execution information location + +To keep *history* after the run (and to drill into past executions): + +. Create an xref:metadata-types/execution-information-location.adoc[Execution information location] (file, caching file, remote Hop Server, …). +. Optionally create an xref:metadata-types/execution-data-profile.adoc[Execution data profile] (first/last/random rows, etc.). +. On the *Native Spark* pipeline run configuration, set both on the main tab. +. Run a pipeline, then open the xref:hop-gui/perspective-execution-information.adoc[Execution Information] perspective. + +image::pipeline/spark/native-spark-logs-to-execution-information-location.png[Execution information logged from a native Spark run,width=90%] + +How data is produced: + +* *State and metrics* (`updateExecutionState`) — computed on the *driver* from `EngineMetrics` / accumulators for all transform types. +* *Row samples* (`registerData`) — collected on *executors* for generic `mapPartitions` transforms when a data profile is set, then merged for the GUI (parent/child execution hierarchy, similar to Beam). + +[IMPORTANT] +==== +If the location is file-based and you need *samples from cluster workers*, the root folder must be *writable from every executor* (shared filesystem or object store). A path that exists only on the driver will still get pipeline/transform *state*, but executor samples will be missing or incomplete. +==== + +On `local[*]`, Spark’s Jackson libraries must not shadow Hop’s Jackson under the plugin classloader. The Spark plugin packaging excludes core Jackson jars so sampling and `HopJson` keep working. + +== How transforms map to Spark + +=== Native handlers + +These use Spark Dataset APIs on the driver graph (no per-row Hop mini-pipeline for the core of the work): + +[options="header",cols="1,2"] +|=== +|Transform |Spark mapping + +|Memory Group By +|`groupBy` + aggregations (SUM, AVG, MIN, MAX, COUNTs, FIRST/LAST, …) + +|Merge Join +|`Dataset.join` (inner / left / right / full outer) + +|Unique Rows +|`dropDuplicates` (optional count via groupBy) + +|Sort Rows +|`orderBy` + +|Spark File Input / Output +|`spark.read` / `df.write` +|=== + +=== Generic mapPartitions transforms + +Most other transforms — Filter Rows, Switch/Case, Stream Lookup, Calculator, Select Values, Dummy, classic file transforms, Excel, and many plugins — run inside `mapPartitions`: + +. Each Spark partition starts a *single-threaded* Hop mini-pipeline for that transform. +. Partition rows are injected; outputs are collected back into a Dataset. +. Plugin JARs used by those transforms must be on the Spark classpath (`dependencies.xml` for the engine plugin, and the fat jar on the cluster). + +=== Info streams and target streams + +*Info streams* (side data):: +Transforms such as *Stream Lookup* need a full secondary dataset while processing the main stream. The engine *broadcasts* the info Dataset to partitions (same idea as Beam `View.asList`). Configure the Stream Lookup *from* transform name exactly. + +*Target streams* (conditional routing):: +*Filter Rows* (true/false) and *Switch/Case* (cases + default) declare named targets. Each input row goes to *one* successor. Downstream hops use keys of the form `Previous - TARGET - Current`. + +*Multiple previous transforms*:: +If several transforms hop into one (branches rejoin), row layouts must match (names, order, types). The engine *unions* those Datasets after a layout check (Beam Flatten equivalent). + +[[limitations-and-design-rules]] +== Limitations and design rules + +Keep these in mind when moving a local pipeline to native Spark. + +[options="header",cols="1,3"] +|=== +|Topic |Behaviour on native Spark + +|Transforms marked *Not Supported* +|See the authoritative list on +xref:pipeline/pipeline-run-configurations/native-spark-pipeline-engine.adoc#transforms-not-supported-on-native-spark[Native Spark Pipeline Engine — Transforms not supported]. +It is derived from each transform page’s *Supported Engines* table (*Native Spark* row). +Examples: sorted *Group By* (use *Memory Group By*), Beam-only I/O, barriers (*Blocking until transforms finish*), result-row hops, *Kafka Consumer*, multi-way / sorted merges. + +|Sorted *Group By* +|*Not supported.* Use *Memory Group By* (native `groupBy`). + +|Copy vs Distribute on multi-hop output +|*Not implemented* (same stance as Beam). Named *target streams* route by condition; plain multi-next hops without target metadata are rejected. Round-robin distribute is not available. + +|Streaming / windows +|Out of scope. Use the Beam engines. + +|Native Spark Kafka +|*Not supported.* There is no Hop transform that maps to Spark’s structured streaming / Kafka source-sink APIs. For Kafka-oriented Beam pipelines, use the Beam engines (e.g. Beam Kafka Consumer/Producer). Hop’s own Kafka transforms (if used) are not native-Spark Dataset I/O. + +|Spark SQL +|*Not supported* as a first-class surface. You cannot submit arbitrary Spark SQL / `DataFrame` queries from Hop metadata; the engine only builds a Dataset graph from the Hop transform graph (plus native handlers for a fixed set of transforms). + +|MLlib / Spark ML +|*Not supported.* There are no native handlers or transforms for training, pipelines, or model inference via Spark MLlib / `spark.ml`. Keep ML workloads in Spark jobs outside Hop, or call external services from ordinary Hop transforms where that fits. + +|Large Stream Lookup tables +|Fully *broadcast* — size carefully. + +|Unique Rows (error stream) +|Reject-to-error style options may not apply; prefer Dataset-oriented dedup semantics. + +|Transform copies / partitioning GUI +|Parallelism comes from *Spark partitions*, not Hop “copies” in the local sense. + +|Plugins in mapPartitions +|Must be present on the Spark classpath (staged list + fat jar). Missing plugins fail at runtime on the executor. + +|Beam-only transforms +|Beam File I/O, Beam Kafka/Kinesis/BigQuery/… are for Beam engines, not native Spark. +|=== + +== Examples from the integration tests + +The Hop source tree includes a self-contained project: + +[source,text] +---- +integration-tests/spark-native/ +---- + +Open that folder as a Hop project (or copy patterns into your own). Workflows are named `main-00xx-*.hwf` and typically: clean output → run the Spark pipeline with `spark-local` → validate on the local engine. + +=== 0001 — Spark File Input / Output + +Minimal path: read a CSV with *Spark File Input*, write with *Spark File Output*. + +image::pipeline/spark/spark-native-0001-input-output.png[0001 input output,width=70%] + +=== 0002 — Memory Group By + +Aggregation via the native Memory Group By handler (Spark `groupBy`). + +image::pipeline/spark/spark-native-0002-group-by.png[0002 group by,width=70%] + +=== 0003 — Filter Rows + +Row filtering in mapPartitions. + +image::pipeline/spark/spark-native-0003-filter-rows.png[0003 filter rows,width=70%] + +=== 0004 — Stream Lookup (info stream) + +Main stream plus a lookup Dataset broadcast into *Stream Lookup* — exercises info streams end-to-end. + +image::pipeline/spark/spark-native-0004-stream-lookup.png[0004 stream lookup,width=75%] + +=== 0005 — Filter Rows with target streams + +True/false targets (conditional fan-out without Copy/Distribute). + +image::pipeline/spark/spark-native-0005-filter-targets.png[0005 filter targets,width=75%] + +=== 0006 — Switch / Case + +Multi-way routing with named cases and default. + +image::pipeline/spark/spark-native-0005-switch-case.png[0006 switch case,width=75%] + +=== 0007 — Classic Text File Input and Spark write-back + +Get File Names → Text File Input (accept filenames) → Spark File Output (e.g. Parquet). Validates classic I/O and partition-safe readback. + +image::pipeline/spark/spark-native-0008-text-file-input-spark-readback.png[0007 text file input spark readback,width=70%] + +=== 0008 — Excel info stream into Stream Lookup + +Main data from multiple Parquet files (*Spark File Input*); product dimension from *Microsoft Excel Input* into *Stream Lookup*; CSV result via *Spark File Output*. + +image::pipeline/spark/spark-native-0008-excel-stream-lookup.png[0008 excel stream lookup,width=75%] + +=== 0099 — Complex sample + +A larger pipeline combining several of the patterns above (good stress check for a full Hop + Spark install). + +image::pipeline/spark/spark-native-0099-complex.png[0099 complex sample,width=95%] + +== Checklist + +* [ ] Java 21; Hop 2.19+ with `plugins/engines/spark` +* [ ] Run configuration engine = *Native Spark* (not Beam Spark) +* [ ] First success on `local[*]` with Spark File Input → transform → Spark File Output +* [ ] Execution information location attached if you need history and samples +* [ ] Cluster: fat jar with `--spark-client-version=native-provided` +* [ ] Cluster: `SPARK_HOME` pinned to Spark *4.1.x* +* [ ] Cluster: metadata JSON exported; run configuration name matches submit args +* [ ] No sorted Group By; no reliance on Copy/Distribute multi-hop behaviour +* [ ] Classic writers use `${Internal.Transform.ID}` (or accept Spark File Output for bulk writes) +* [ ] Lakehouse (optional): confirm `plugins/engines/spark/lib/{delta,iceberg}` present; see xref:pipeline/spark/lakehouse.adoc[lakehouse guide] + +== Related pages + +* xref:pipeline/pipeline-run-configurations/native-spark-pipeline-engine.adoc[Native Spark pipeline engine] (option reference) +* xref:pipeline/spark/lakehouse.adoc[Lakehouse tables on the native Spark engine] (Delta / Iceberg) +* xref:pipeline/pipeline-run-configurations/beam-spark-pipeline-engine.adoc[Beam Spark pipeline engine] (contrast) +* xref:pipeline/beam/getting-started-with-beam.adoc[Getting started with Apache Beam] +* xref:pipeline/pipeline-run-configurations/pipeline-run-configurations.adoc[Pipeline run configurations] +* xref:metadata-types/execution-information-location.adoc[Execution information location] +* xref:metadata-types/execution-data-profile.adoc[Execution data profile] +* xref:metadata-types/spark-catalog.adoc[Spark Catalog] +* xref:hop-gui/perspective-execution-information.adoc[Execution Information perspective] +* xref:hop-tools/hop-conf/hop-conf.adoc[hop-conf] (fat jar, metadata export) diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/lakehouse.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/lakehouse.adoc new file mode 100644 index 00000000000..57b850ee327 --- /dev/null +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/lakehouse.adoc @@ -0,0 +1,314 @@ +//// +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. +//// +[[NativeSparkLakehouse]] +:imagesdir: ../../../assets/images +:description: Open table formats (Delta Lake and Apache Iceberg) on the Apache Hop native Spark pipeline engine +:toc: +:toclevels: 3 + += Lakehouse tables on the native Spark engine + +This guide covers **Delta Lake** and **Apache Iceberg** support on Hop’s xref:pipeline/spark/getting-started-with-native-spark.adoc[native Spark pipeline engine] (Spark *4.1.x*, no Beam). + +It assumes you already have a working *Native Spark* pipeline run configuration (`local[*]` or cluster `spark-submit`). Classic file formats (CSV, Parquet, JSON, ORC, text) stay on *Spark File Input* / *Spark File Output*. Lakehouse work uses dedicated **Lake Table** transforms. + +== What you get + +[options="header",cols="2,1"] +|=== +| Capability | How + +| Read / write ACID tables | xref:pipeline/transforms/spark-lake-table-input.adoc[Spark Lake Table Input] / xref:pipeline/transforms/spark-lake-table-output.adoc[Spark Lake Table Output] + +| Path-based tables | Identifier mode **PATH** (local, HDFS, object-store URIs Spark can open) + +| Catalog tables | Identifier mode **TABLE** + xref:metadata-types/spark-catalog.adoc[Spark Catalog] metadata (Iceberg-first). Use **Load catalog template** on the metadata editor for Hadoop/REST/Hive/Glue/Nessie skeletons. + +| Time travel | Version / snapshot id or timestamp on Input + +| Upsert | xref:pipeline/transforms/spark-lake-table-merge.adoc[Spark Lake Table Merge] (`MERGE INTO`) + +| Compaction / vacuum / expire | xref:pipeline/transforms/spark-lake-table-maintenance.adoc[Spark Lake Table Maintenance] +|=== + +*Not in scope:* Hudi; Beam Spark lakehouse. + +== Version pins + +[options="header",cols="2,1"] +|=== +| Component +| Coordinate / version + +| Spark (Hop native engine) +| `spark-sql_2.13` *4.1.2* (Scala *2.13*) + +| Delta Lake +| `io.delta:delta-spark_4.1_2.13:*4.3.1*` + +| Apache Iceberg +| `org.apache.iceberg:iceberg-spark-runtime-4.1_2.13:*1.11.0*` + +|=== + +The `_4.1_2.13` suffix means **Spark 4.1 / Scala 2.13**, not the product major version of Delta or Iceberg. + +== Connectors (shipped by default) + +Default Hop client assemblies package Delta Lake and Apache Iceberg under the **Spark engine plugin classloader**: + +[source,shell] +---- +$HOP_HOME/plugins/engines/spark/lib/delta/ # Delta jars +$HOP_HOME/plugins/engines/spark/lib/iceberg/ # Iceberg runtime jar +---- + +`SparkPipelineEngine` builds `SparkSession` under that classloader (`lib/` recursive + `dependencies.xml` folders). A sibling plugin folder alone is **not** enough. + +hop-run / Hop Gui (`local[*]`) therefore need **no manual connector install** after a normal Hop install or rebuild of `plugins/engines/spark`. + +=== Shipped jar list + +[options="header",cols="2,1"] +|=== +|Jar |Role + +|`delta-spark_4.1_2.13-4.3.1.jar` +|Delta Spark SQL / DataSource + +|`delta-storage-4.3.1.jar` +|Delta storage layer + +|`delta-kernel-api-4.3.1.jar` +|Delta Kernel API + +|`delta-kernel-defaults-4.3.1.jar` +|Delta Kernel defaults + +|`delta-kernel-unitycatalog-4.3.1.jar` +|Transitive from delta-spark 4.3.1 + +|`unitycatalog-client-0.5.0.jar` +|Transitive (UC-related) + +|`unitycatalog-hadoop-0.5.0.jar` +|Transitive (UC-related) + +|`iceberg-spark-runtime-4.1_2.13-1.11.0.jar` +|Shaded Iceberg Spark runtime +|=== + +Jackson, Spark, Scala, and slf4j are **not** duplicated into these folders (Hop parent CL + existing spark lib provide them). + +=== spark-submit (cluster) + +Prefer a **native-provided** fat jar (Hop + plugins + lakehouse connectors — **not** Spark). Fat-jar generation walks `plugins/engines/spark/lib` (including `delta/` and `iceberg/`) and keeps those connectors while excluding the Spark runtime. + +[source,bash] +---- +export SPARK_HOME=/path/to/spark-4.1.x-bin-hadoop3 + +./hop-conf.sh --generate-fat-jar=/tmp/hop-native-provided.jar \ + --spark-client-version=native-provided + +# Export project metadata (run config + any Spark Catalog entries) to JSON, then: +"${SPARK_HOME}/bin/spark-submit" \ + --master spark://your-master:7077 \ + --conf spark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension,org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions \ + --conf spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog \ + --class org.apache.hop.spark.run.MainSpark \ + /tmp/hop-native-provided.jar \ + /path/to/pipeline.hpl \ + /path/to/metadata.json \ + YourNativeSparkRunConfig +---- + +Optional: if you strip connectors from the fat jar or use site-managed `$SPARK_HOME/jars`, you can still add: + +---- +--packages io.delta:delta-spark_4.1_2.13:4.3.1,org.apache.iceberg:iceberg-spark-runtime-4.1_2.13:1.11.0 +---- + +When both formats are used, keep **Delta** on `spark_catalog` and register Iceberg catalogs under **other** names (for example `lake`). Export full project metadata so `SparkCatalog` entries travel with the job. + +If connectors are missing, Hop fails with an actionable `HopException` (`SparkLakeConnectorProbe`). + +== Session configuration + +Hop’s `LakeSessionPlan` scans Lake Table transforms before the session is used and applies the conf needed for hop-run. Under pure `spark-submit`, extensions and catalogs must already be correct on the active session (CLI `--conf`, site defaults, or exported metadata that Hop can still register). + +[options="header",cols="1,2"] +|=== +|When |Session conf (summary) + +|Any Delta use (PATH or TABLE) +|`spark.sql.extensions` includes `io.delta.sql.DeltaSparkSessionExtension` **and** `spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog` + +|Any Iceberg PATH use +|Iceberg extensions + built-in Hadoop catalog **`hop_iceberg`** (`type=hadoop`, warehouse under tmp) + +|Iceberg TABLE +|`spark.sql.catalog.=org.apache.iceberg.spark.SparkCatalog` (+ `type`, `warehouse` / `uri`, …) from xref:metadata-types/spark-catalog.adoc[Spark Catalog] metadata +|=== + +IMPORTANT: Delta PATH I/O on Spark 4.1 + Delta 4.3 **requires DeltaCatalog**, not only the extension. Iceberg bare `format("iceberg").load(path)` is **not** used — it defaults toward HiveCatalog and fails without HMS. PATH mode uses `hop_iceberg.\`file:///…\`` (or equivalent URI) instead. + +[[path-vs-table]] +== PATH vs TABLE + +[options="header",cols="1,2,2"] +|=== +|Mode +|Identifier +|Typical use + +|**PATH** +|Directory / table root Spark can open (`file:///…`, `s3a://…`, …) — a **Spark/Hadoop URI**, not a Hop VFS path (`s3://`, named MinIO, …). See xref:pipeline/spark/paths-and-filesystems.adoc[Paths and file systems]. +|Local lakes, object-store folders, quick prototypes + +|**TABLE** +|Multi-part name (`catalog.namespace.table`) + optional Spark Catalog metadata name +|Production catalogs (Iceberg Hadoop warehouse, REST, …) + +|=== + +[options="header",cols="3"] +|=== +| Format +| PATH read +| PATH write + +| **Delta** +| `spark.read.format("delta").load(path)` +| `format("delta").mode(…).save(path)` + +| **Iceberg** +| SQL over `hop_iceberg.uri` +| `writeTo(hop_iceberg.uri).using("iceberg")` + +|=== + + +[options="header",cols="3"] +|=== +| Format +| TABLE read +| TABLE write + +| **Iceberg** (primary) +| `SELECT * FROM catalog.ns.table` +| `writeTo(id).using("iceberg")` + +| **Delta** (advanced) +| `format("delta").table(id)` +| `format("delta").saveAsTable(id)` + + +|=== + + +== Lake Table transforms + +All four live under the **Big Data** category and are **native Spark only** (not Beam Spark, not the local Hop engine). + +* xref:pipeline/transforms/spark-lake-table-input.adoc[Spark Lake Table Input] — read, optional projection, time travel +* xref:pipeline/transforms/spark-lake-table-output.adoc[Spark Lake Table Output] — append / overwrite / …; **default save mode is ErrorIfExists** +* xref:pipeline/transforms/spark-lake-table-merge.adoc[Spark Lake Table Merge] — single upstream hop is the source; target is PATH or TABLE +* xref:pipeline/transforms/spark-lake-table-maintenance.adoc[Spark Lake Table Maintenance] — zero-input action (OPTIMIZE, VACUUM, expire, …) + +Writes, merges, and maintenance run as Spark **actions** during graph materialisation and register empty leaf Datasets so a later engine `count()` does not re-execute them (same contract as *Spark File Output*). + +=== Topology rules + +* **Output / Merge** need exactly one main-stream upstream hop (the data to write or the MERGE source). +* **Maintenance** is a zero-input sink — no hop required. +* Prefer a single Lake Table Output / Merge / Maintenance leaf per branch; avoid graphs that would re-trigger the same ACID action. +* Do not use *Spark File Output* with format `delta` / `iceberg` — use Lake Table transforms so session plan, probes, and SQL identifiers stay consistent. + +=== Metadata injection (MDI) + +All four Lake Table transforms support xref:pipeline/metadata-injection.adoc[metadata injection] so complex multi-table landings, CDC upserts, and fleet maintenance can use **one template pipeline** driven by an injecting pipeline (table path/identifier, format, merge condition, OPTIMIZE/VACUUM parameters, etc.). + +Injection keys are documented on each transform page. Shared identity keys: `FORMAT`, `IDENTIFIER_MODE`, `TABLE_PATH`, `TABLE_IDENTIFIER`, `CATALOG_METADATA_NAME`. + +=== Quick PATH round-trip (Delta) + +. Install Delta jars (see above) and restart Hop. +. Create a *Native Spark* run configuration (`local[*]`). +. Pipeline: *Row Generator* (or *Spark File Input*) → *Spark Lake Table Output* + ** Format: `delta` + ** Identifier mode: `PATH` + ** Table path: e.g. `${PROJECT_HOME}/output/delta-orders` + ** Save mode: `Overwrite` for a first write (default is *ErrorIfExists*) +. Second pipeline or hop: *Spark Lake Table Input* on the same path → *Spark File Output* or preview. +. Run with the native Spark run configuration. + +=== Time travel sketch + +Write twice with *Overwrite*, then set Input **Time travel** to `VERSION` and **Version** to `0` (Delta) or the first snapshot id from Iceberg’s `{table}.snapshots`. See the Input transform page for TIMESTAMP mode. + +=== MERGE sketch + +*Spark File Input* / generator → *Spark Lake Table Merge*: + +* Target: PATH or TABLE of an existing Delta/Iceberg table +* Merge condition: e.g. `t.id = s.id` (`t` = target, `s` = source) +* When matched: `UPDATE_ALL`; when not matched: `INSERT_ALL` + +=== Maintenance sketch + +Standalone *Spark Lake Table Maintenance*: + +* Operation `OPTIMIZE` (safe compaction) +* For `VACUUM` / `EXPIRE_SNAPSHOTS` / `DELETE_WHERE`: set retention / WHERE as required and tick **Acknowledge destructive** + +== Troubleshooting + +[options="header",cols="1,2"] +|=== +|Symptom |What to check + +|Probe / classpath error naming Delta or Iceberg classes +|Standard install has jars under `plugins/engines/spark/lib/{delta,iceberg}`? Rebuild engines-spark / reinstall Hop? Fat jar built with current install? Optional cluster `--packages` / site jars? + +|`DELTA_CONFIGURE_SPARK_SESSION_WITH_EXTENSION_AND_CATALOG` +|`spark_catalog` must be `DeltaCatalog` when using Delta (hop-run sets this when Delta transforms are present) + +|Iceberg PATH fails with HiveCatalog / HMS +|Do not use bare `format("iceberg")` outside Hop; ensure Iceberg transforms run under hop-run so `hop_iceberg` is registered, or pass equivalent conf on submit + +|TABLE unresolved +|Spark Catalog metadata exported? Catalog name matches the first segment of `catalog.ns.table`? Warehouse / URI reachable from driver and executors? + +|Destructive maintenance rejected +|Set retention hours / WHERE and enable acknowledge flag + +|Writes seem to run twice +|Only one action sink should materialise the write; check for extra Output hops or dual engine re-runs +|=== + +== Related pages + +* xref:pipeline/spark/getting-started-with-native-spark.adoc[Getting started with the native Spark pipeline engine] +* xref:pipeline/pipeline-run-configurations/native-spark-pipeline-engine.adoc[Native Spark pipeline engine] +* xref:metadata-types/spark-catalog.adoc[Spark Catalog] +* xref:pipeline/transforms/spark-lake-table-input.adoc[Spark Lake Table Input] +* xref:pipeline/transforms/spark-lake-table-output.adoc[Spark Lake Table Output] +* xref:pipeline/transforms/spark-lake-table-merge.adoc[Spark Lake Table Merge] +* xref:pipeline/transforms/spark-lake-table-maintenance.adoc[Spark Lake Table Maintenance] +* Developer notes and spike findings: `plugins/engines/spark/README.md` in the Hop source tree +* Integration tests: `integration-tests/lakehouse` (Delta) and `integration-tests/iceberg` (Iceberg) in the Hop source tree diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/paths-and-filesystems.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/paths-and-filesystems.adoc new file mode 100644 index 00000000000..b98f8bcc256 --- /dev/null +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/paths-and-filesystems.adoc @@ -0,0 +1,312 @@ +//// +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. +//// +[[NativeSparkPathsAndFilesystems]] +:imagesdir: ../../../assets/images +:description: Hop VFS URIs versus Spark/Hadoop FileSystem URIs when using the native Spark pipeline engine. + += Paths and file systems on native Spark + +Apache Hop uses *two different path languages*. They look similar (both use `scheme://…` URIs) but resolve through different stacks, credentials, and plugins. Mixing them is a common source of confusion when you move from the local engine to the xref:pipeline/spark/getting-started-with-native-spark.adoc[native Spark pipeline engine]. + +== Two dialects + +[options="header",cols="1,2,2,2"] +|=== +|Dialect +|Resolved by +|Typical schemes +|Used for + +|**Hop VFS URI** +|Apache Commons VFS via `HopVfs` and Hop VFS plugins +|`file://`, `s3://`, `azure://` / `azfs://`, `gs://`, `googledrive://`, `dropbox://`, `webdav4(s)://`, **named** connections such as `minio://` or `myconn://` +|Hop GUI file dialog, classic transforms (Text File Input/Output, Get File Names, …), loading pipeline/metadata on the Spark driver + +|**Spark URI** (Hadoop FileSystem URI) +|Spark SQL Dataset I/O → Hadoop `FileSystem` on the *driver and every executor* +|`file:///…`, `hdfs://…`, **`s3a://…`**, `abfs://…` / `wasbs://…`, connector-specific `gs://…`, … +|xref:pipeline/spark/getting-started-with-native-spark.adoc#_building_spark_oriented_pipelines[Spark File Input/Output], Lake Table **PATH** mode, Iceberg/Delta warehouse locations +|=== + +[IMPORTANT] +==== +A path that opens in the Hop *File* dialog (Hop VFS) is *not* automatically valid for *Spark File Input*. +Spark never calls `HopVfs` for Dataset `load` / `save`. The engine only resolves Hop variables, then hands the string to Spark. +==== + +For the full list of Hop VFS schemes, see xref:vfs.adoc[Virtual File Systems]. + +== When each stack is used on native Spark + +[options="header",cols="2,1,3"] +|=== +|Feature |Path dialect |Notes + +|Spark File Input / Spark File Output +|**Spark URI** +|`spark.read…load(path)` / `df.write…save(path)` + +|Spark Lake Table (PATH mode) +|**Spark URI** +|e.g. `file:///…`, `s3a://bucket/table` — see xref:pipeline/spark/lakehouse.adoc[Lakehouse] + +|Spark Catalog warehouse / Iceberg `io-impl` +|**Spark URI** +|Templates use `s3a://` and `file:///` + +|Classic transforms via `mapPartitions` (Text File Input, Excel, …) +|**Hop VFS URI** +|Each partition runs a small Hop pipeline; VFS plugins and named connections work if the *executor* classpath and credentials allow it + +|`MainSpark` loading `.hpl` / metadata JSON +|**Hop VFS URI** (driver only) +|Driver must open the definition files; *data* paths still must work on executors +|=== + +== Scheme collision matrix + +[options="header",cols="1,2,2,2"] +|=== +|You write |Hop VFS meaning |Spark meaning |What to do on native Spark File/Lake + +|`s3://bucket/key` +|AWS S3 via Hop’s S3 VFS plugin (AWS SDK) +|Often legacy or unused; production Spark expects **S3A** +|Use **`s3a://bucket/key`** and configure `spark.hadoop.fs.s3a.*` (or cluster `core-site`) + +|`s3a://bucket/key` +|**Not** a Hop VFS scheme (unless you add a bridge plugin) +|Hadoop S3A FileSystem +|Correct for Spark; configure IAM / keys / endpoint for S3A + +|`minio:///bucket/…` or `myconn://…` +|Named MinIO or other VFS connection metadata +|Unknown scheme unless Hadoop is registered for that name +|Do *not* use named VFS schemes in Spark File path. Use `s3a://` + `fs.s3a.endpoint` (and path-style access) for MinIO-compatible stores + +|`azure://` / `azfs://` +|Hop Azure VFS +|Spark usually wants **`abfs://`** or **`wasbs://`** +|Use Azure Hadoop schemes + connector conf for Dataset I/O + +|`gs://` +|Hop Google Cloud Storage VFS +|Spark GCS connector (same scheme, *different* jars/credentials) +|Works only if the Spark GCS connector and credentials are on the cluster + +|`$\{PROJECT_HOME\}/data.csv` +|Resolved, then Hop VFS or local rules +|Resolved, then Hadoop default FS / local path +|On cluster submit, set `PROJECT_HOME` in the env config to a **Spark-visible** URI (for object storage prefer `s3a://…`) +|=== + +== MinIO and S3-compatible storage + +Hop’s xref:metadata-types/minio-connection.adoc[Minio Connection] metadata registers the **connection name** as a VFS scheme (for example connection `minio` → `minio:///demo/…`). That path works with classic Hop transforms and the file dialog. + +On **Spark File Input/Output** and **Lake PATH**: + +. Put objects under a URI Spark’s S3A client can open, typically `s3a://bucket/prefix/…`. +. Point S3A at your endpoint with run-configuration *Spark config* lines, for example: + +[source] +---- +spark.hadoop.fs.s3a.endpoint=https://minio.example.com:9000 +spark.hadoop.fs.s3a.path.style.access=true +spark.hadoop.fs.s3a.access.key=... +spark.hadoop.fs.s3a.secret.key=... +---- + +(Exact keys depend on your Hadoop/Spark version and security model; prefer IAM roles on cloud clusters over embedding keys when possible.) + +[[spark-project-package]] +== Project package (nested pipelines / mappings) + +Native Spark ships only the *top-level* pipeline XML plus metadata JSON by default. Transforms such as *Simple Mapping* and *Pipeline Executor* load child `.hpl` files at runtime via Hop VFS using paths like `$\{PROJECT_HOME\}/mappings/child.hpl`. On a cluster those files must exist where every executor can open them. + +=== Export a Spark project package + +This package is **specifically for Native Spark execution** (nested Simple Mapping / Pipeline Executor on the cluster). It is not the same as *File → Export current project to zip*. + +**GUI:** *Tools → Export project package for Native Spark…* (requires an active project / `PROJECT_HOME`). + +**CLI:** + +[source,bash] +---- +# Preferred for scripts (no project registration required): +./hop-conf.sh --export-spark-project=/tmp/my-project-spark.zip \ + --export-spark-project-home=/path/to/project + +# Or enable a registered project first (-j, not --project/-p): +./hop-conf.sh -j my-project --export-spark-project=/tmp/my-project-spark.zip +---- + +(Short option for the zip path: `-esp`.) + +Zip layout: + +* `metadata.json` — Hop metadata (run configs, connections, catalogs) +* `hop-spark-package.properties` — marker (`projectRoot=project`) +* `project/…` — project home files (see filters below) + +==== Package include / exclude filters + +Export does **not** read `.gitignore`. Built-in skipped directory basenames (case-insensitive): + +`work`, `datasets`, `target`, `.git`, `node_modules`, `.idea`, `.settings` + +Configure extras with **`spark-package.json`** in the project home (Tools → *Edit Native Spark package filters…* in Hop GUI, or edit the file by hand): + +[source,json] +---- +{ + "excludeDirs": [ "tmp", "screenshots" ], + "excludeGlobs": [ "**/*.jar", "**/*.zip" ], + "includePaths": [ "work/cluster-env.json" ], + "replaceDefaultExcludeDirs": false +} +---- + +|=== +|Field |Meaning + +|`excludeDirs` +|Extra directory basenames to skip (merged with built-ins unless replace is true) + +|`excludeGlobs` +|Globs relative to project home (e.g. `**/*.jar`) + +|`includePaths` +|Relative paths force-included even under a skipped dir + +|`replaceDefaultExcludeDirs` +|If true, only the listed `excludeDirs` apply (built-ins off) +|=== + +**hop-conf** (merged on top of `spark-package.json`): + +[source,bash] +---- +./hop-conf.sh --export-spark-project=/tmp/pkg.zip \ + --export-spark-project-home=/path/to/project \ + --export-spark-project-exclude-dirs=tmp,screenshots \ + --export-spark-project-exclude-globs='**/*.jar' \ + --export-spark-project-include=work/cluster-env.json +---- + +Databricks Deploy & run auto-export uses the same rules (reads `spark-package.json` when present). + +=== Run with MainSpark + +[source,bash] +---- +"${SPARK_HOME}/bin/spark-submit" \ + --class org.apache.hop.spark.run.MainSpark \ + /tmp/hop-native-spark4-submit.jar \ + --HopProjectPackage=/tmp/my-project-spark.zip \ + --HopPipelinePath=pipelines/run-on-spark.hpl \ + --HopRunConfigurationName=YourNativeSparkRunConfig \ + [--HopConfigFile=/path/cluster-env.json] +---- + +**Databricks Deploy & run:** the xref:workflow/actions/databricks-job-run.adoc[Databricks Job Run] action can **upload** this package (export from `PROJECT_HOME` or an existing zip) and an optional environment config file to the Volume base path, then pass the same named MainSpark arguments on the JAR task. Enable **Upload Spark project package** on the action when the pipeline needs nested definitions. + +On Databricks, packages under **`/Volumes/…`** (or `/dbfs/…`) are treated as **cluster-shared**: Hop does **not** call `SparkContext.addFile` for them. Executors open the same Volume path (UC FUSE mount). That avoids failures such as `Failed to fetch spark://…/files/hop-spark-package.zip` / `Stream '/files/…' was not found` when the driver cannot serve SparkFiles to job-cluster workers. + +Flow: + +. Driver materializes the package under `${java.io.tmpdir}/hop-spark-pkg-*/` and sets `PROJECT_HOME`. +. When the Native Spark session starts, the engine calls `SparkContext.addFile` so **every executor** receives the zip (`SparkFiles`). +. Each mini-pipeline init (`HopMapPartitionsFn`) resolves the zip via `SparkFiles.get`, extracts once per worker JVM, and sets `PROJECT_HOME` for Hop VFS loads. + +Variables: + +* `HOP_SPARK_PROJECT_PACKAGE` — local path to the zip (after staging) +* `HOP_SPARK_PROJECT_PACKAGE_SPARK_FILE` — basename for `SparkFiles.get` +* `PROJECT_HOME` — extracted definition root on that JVM + +[IMPORTANT] +==== +With a project package, **`PROJECT_HOME` is the extracted definition tree**, not your object-store data root. + +* Nested mappings: `$\{PROJECT_HOME\}/mappings/…` ✅ +* Spark File Input data: use `HOP_DATA`, `OUTPUT_ROOT`, or explicit `s3a://…` / `hdfs://…` — **not** `$\{PROJECT_HOME\}/files/…` unless those files are small resources inside the package and you accept local extract paths. + +The path scheme map (`s3=s3a`) is independent: it rewrites Dataset schemes only. +==== + +Limitations: Workflow Executor and dynamic Pipeline Executor filenames remain experimental. The package must be readable on the *driver* at submit time so `addFile` can stage it (local path or VFS URI Hop can open). + +=== Path scheme map (run configuration) + +The Native Spark pipeline run configuration includes an optional **Path scheme map** field: multi-line `from=to` entries applied **only** to *Spark File Input/Output* and *Lake Table PATH* URIs after variable resolution (not to classic Hop VFS / mapPartitions paths). + +Example: + +[source] +---- +# Hop VFS / named schemes -> Spark/Hadoop schemes +s3=s3a +minio=s3a +azure=abfs +---- + +* Tokens may be written as `s3` or `s3://` (normalized to bare scheme names). +* Lines starting with `#` are comments. +* Mapping only rewrites the scheme; it does **not** inject credentials or endpoints — still set `spark.hadoop.fs.s3a.*` (or other FS config) for the *target* scheme. +* Prefer putting the target URI in pipeline variables when you can; use the map when the same pipeline must keep Hop-style schemes in metadata. + +== Cluster environment variables + +When you `spark-submit` with an environment configuration file, remap project variables to **Spark URIs**, not Hop-only VFS schemes: + +[source,json] +---- +{ + "variables": [ + { + "name": "PROJECT_HOME", + "value": "s3a://hop-project/", + "description": "Project root on object storage (Hadoop S3A URI for executors)" + }, + { + "name": "OUTPUT_ROOT", + "value": "s3a://hop-project/output/", + "description": "Shared output prefix visible to all executors" + } + ] +} +---- + +See xref:pipeline/spark/getting-started-with-native-spark.adoc[Getting started with the native Spark engine] for the full submit flow. + +== Practical rules of thumb + +. **Spark File / Lake path fields** → use **Spark URIs** (`file:///`, `hdfs://`, `s3a://`, `abfs://`, …). +. **Classic Hop file transforms** (even on the Spark engine) → use **Hop VFS URIs** and ensure plugins/credentials exist on executors. +. Prefer *Spark File Output* for large distributed writes; classic Text File Output is for POCs and needs unique names per partition (`${Internal.Transform.ID}`). +. Do not assume `s3://` means the same thing in Hop and Spark — in Hop docs for object storage on Spark, **`s3a://` is canonical**. + +== Related pages + +* xref:pipeline/spark/getting-started-with-native-spark.adoc[Getting started with the native Spark engine] +* xref:pipeline/spark/lakehouse.adoc[Lakehouse tables (Delta / Iceberg)] +* xref:pipeline/pipeline-run-configurations/native-spark-pipeline-engine.adoc[Native Spark pipeline engine] +* xref:vfs.adoc[Virtual File Systems] +* xref:metadata-types/minio-connection.adoc[Minio Connection] +* xref:metadata-types/spark-catalog.adoc[Spark Catalog] diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms.adoc index bbb11f3fa95..5c2b495a40f 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms.adoc @@ -208,6 +208,10 @@ The pages nested under this topic contain information on how to use the transfor * xref:pipeline/transforms/sort.adoc[Sort Rows] * xref:pipeline/transforms/sortedmerge.adoc[Sorted Merge] * xref:pipeline/transforms/sortedschemamerge.adoc[Sorted Schema Merge] +* xref:pipeline/transforms/spark-lake-table-input.adoc[Spark Lake Table Input] +* xref:pipeline/transforms/spark-lake-table-maintenance.adoc[Spark Lake Table Maintenance] +* xref:pipeline/transforms/spark-lake-table-merge.adoc[Spark Lake Table Merge] +* xref:pipeline/transforms/spark-lake-table-output.adoc[Spark Lake Table Output] * xref:pipeline/transforms/splitfields.adoc[Split Fields] * xref:pipeline/transforms/splitfieldtorows.adoc[Split fields to rows] * xref:pipeline/transforms/splunkinput.adoc[Splunk Input] diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/abort.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/abort.adoc index b445447c681..8909014d74e 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/abort.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/abort.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Abort tranform aborts a running pipeline as soon as it receives input data. The main use case for this transform is to throw an error when an unexpected or unwanted situation occurs. +:description: The Abort tranform aborts a running pipeline. = image:transforms/icons/abort.svg[Abort Icon, role="image-doc-icon"] Abort @@ -34,9 +34,11 @@ For example, you can use this transform so that a pipeline can be aborted after [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/accessoutput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/accessoutput.adoc index ba019468298..da2214b151c 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/accessoutput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/accessoutput.adoc @@ -32,9 +32,11 @@ The Microsoft Access Output allows you to create a new Access database file as a [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addchecksum.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addchecksum.adoc index 4ec4389eea1..c65ed5e6b91 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addchecksum.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addchecksum.adoc @@ -32,9 +32,11 @@ The Add a Checksum transform calculates checksums for one or more fields in the [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addconstant.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addconstant.adoc index 6d3f5368312..a9f0d2819cb 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addconstant.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addconstant.adoc @@ -31,9 +31,11 @@ The Add Constant Values transform is a simple and high performance way to add co [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addfieldschangesequence.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addfieldschangesequence.adoc index a73e568898d..e4981ff98c9 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addfieldschangesequence.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addfieldschangesequence.adoc @@ -31,9 +31,11 @@ The Add Fields Changing Sequence transform simply adds a sequence value which re [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:cross.svg[Not Supported, 24] -!Flink! image:cross.svg[Not Supported, 24] -!Dataflow! image:cross.svg[Not Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addsequence.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addsequence.adoc index 53f52252f55..41c86337b63 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addsequence.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addsequence.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Add Sequence transform adds a sequence to the Hop stream. A sequence is an ever-changing integer value with a specific start and increment value. Sequences can be generated by Hop or retrieved from a database. +:description: The Add Sequence transform adds a sequence to the Hop stream. = image:transforms/icons/addsequence.svg[Add Sequence Icon, role="image-doc-icon"] Add Sequence @@ -37,9 +37,11 @@ Also, they are not stored, so the values start back at the same value every time [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:cross.svg[Not Supported, 24] -!Flink! image:cross.svg[Not Supported, 24] -!Dataflow! image:cross.svg[Not Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addxml.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addxml.adoc index 3d36ef62183..3df3de478e5 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addxml.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/addxml.adoc @@ -32,9 +32,11 @@ This XML is added to the row in the form of a String field. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/analyticquery.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/analyticquery.adoc index 2d4e8e28263..31463785dc4 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/analyticquery.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/analyticquery.adoc @@ -36,9 +36,11 @@ Examples of common use cases are: [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:cross.svg[Not Supported, 24] -!Flink! image:cross.svg[Not Supported, 24] -!Dataflow! image:cross.svg[Not Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/apache-tika.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/apache-tika.adoc index 6e2c8e0fe4c..38dcfa23244 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/apache-tika.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/apache-tika.adoc @@ -35,9 +35,11 @@ If you need specific pieces of information from this metadata, you can extract t [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/append.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/append.adoc index 4b9fd821486..82baaf16d07 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/append.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/append.adoc @@ -35,9 +35,11 @@ As always, the row layout for the input data coming from both transforms has to [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:cross.svg[Not Supported, 24] -!Flink! image:cross.svg[Not Supported, 24] -!Dataflow! image:cross.svg[Not Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/avro-decode.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/avro-decode.adoc index d0d899195fe..ace9b35b181 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/avro-decode.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/avro-decode.adoc @@ -31,9 +31,11 @@ The Avro Decode transform allows you to decode an Avro field and convert it to H [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/avro-encode.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/avro-encode.adoc index 81058f44e9a..859c2439d3f 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/avro-encode.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/avro-encode.adoc @@ -31,9 +31,11 @@ The Avro Encode transform allows you to encode a new Avro Record field using a s [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/avro-file-input.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/avro-file-input.adoc index 5893f773e54..71dd5779c65 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/avro-file-input.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/avro-file-input.adoc @@ -33,9 +33,11 @@ Each record is encapsulated in an Avro field, each value has its own Schema and [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/avro-file-output.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/avro-file-output.adoc index e75f136caae..10de2c9c695 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/avro-file-output.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/avro-file-output.adoc @@ -31,9 +31,11 @@ The Avro File Output transform can write Apache Avro messages to a file or field [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/aws-sns-notify.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/aws-sns-notify.adoc index e90f8fa5f5c..e02b1859c2a 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/aws-sns-notify.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/aws-sns-notify.adoc @@ -31,9 +31,11 @@ The AWS SNS Notify transform enables you to send notifications from an Apache Ho [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Supported, 24] -!Flink! image:question_mark.svg[Supported, 24] -!Dataflow! image:question_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/aws-sqs-reader.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/aws-sqs-reader.adoc index 1453f08f6e2..e479b85591b 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/aws-sqs-reader.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/aws-sqs-reader.adoc @@ -31,9 +31,11 @@ The AWS SQS Reader transform enables you to receive messages from Amazon Web Ser [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Supported, 24] -!Flink! image:question_mark.svg[Supported, 24] -!Dataflow! image:question_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/azure-event-hubs-listener.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/azure-event-hubs-listener.adoc index 8526768895e..b21d7b3c992 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/azure-event-hubs-listener.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/azure-event-hubs-listener.adoc @@ -31,9 +31,11 @@ The Azure Event Hubs Listener transform listens indefinitely to an Event Hub on [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:cross.svg[Not Supported, 24] -!Flink! image:cross.svg[Not Supported, 24] -!Dataflow! image:cross.svg[Not Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/azure-event-hubs-writer.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/azure-event-hubs-writer.adoc index 5cd411b7594..c54184b6dc6 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/azure-event-hubs-writer.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/azure-event-hubs-writer.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Azure Event Hubs Writer transform allows you to write messages (events) to a streaming service bus called Event Hubs on the Microsoft Azure cloud platform. +:description: The Azure Event Hubs Writer transform allows you to write messages (events) to Event Hubs on the Microsoft Azure cloud platform. = image:transforms/icons/azure.svg[Azure Event Hubs Writer Icon, role="image-doc-icon"] Azure Event Hubs Writer @@ -31,9 +31,11 @@ The Azure Event Hubs Writer transform allows you to write messages (events) to a [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:cross.svg[Not Supported, 24] -!Flink! image:cross.svg[Not Supported, 24] -!Dataflow! image:cross.svg[Not Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beambigqueryinput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beambigqueryinput.adoc index 85909cb6eb5..87ec43301c4 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beambigqueryinput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beambigqueryinput.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The BigQuery Input transform reads data from Google Cloud BigQuery. It runs on the local Hop engine as well as every Beam engine (Direct, Dataflow, Spark, Flink). +:description: The BigQuery Input transform reads data from Google Cloud BigQuery. = image:transforms/icons/beam-bq-input.svg[BigQuery Input Icon, role="image-doc-icon"] BigQuery Input @@ -31,9 +31,11 @@ The BigQuery Input transform reads data from a link:https://cloud.google.com/big [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beambigqueryoutput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beambigqueryoutput.adoc index bc1576b1012..e6e7b9e48bb 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beambigqueryoutput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beambigqueryoutput.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The BigQuery Output transform writes data to a Google Cloud BigQuery table. It runs on the local Hop engine (via streaming inserts) as well as every Beam engine (Direct, Dataflow, Spark, Flink). +:description: The BigQuery Output transform writes data to a Google Cloud BigQuery table. = image:transforms/icons/beam-bq-output.svg[BigQuery Output Icon, role="image-doc-icon"] BigQuery Output @@ -31,9 +31,11 @@ The BigQuery Output transform writes data to a link:https://cloud.google.com/big [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beambigtableinput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beambigtableinput.adoc index b63fc971a00..b0ec391d678 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beambigtableinput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beambigtableinput.adoc @@ -32,9 +32,11 @@ The Beam Bigtable Input transform can be used to input data from link:https://cl [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:cross.svg[Not Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:cross.svg[Not Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beambigtableoutput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beambigtableoutput.adoc index d7c1c8889f4..b5e21b712f8 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beambigtableoutput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beambigtableoutput.adoc @@ -32,9 +32,11 @@ The Beam Bigtable Output transform can be used to write data to a link:https://c [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:cross.svg[Not Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:cross.svg[Not Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamfileinput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamfileinput.adoc index 2283f0819b4..58bb80918db 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamfileinput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamfileinput.adoc @@ -32,9 +32,11 @@ The Beam File Input transform reads files using a file definition with the Beam [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:cross.svg[Not Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:cross.svg[Not Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamfileoutput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamfileoutput.adoc index 391ac6d83b2..48865021170 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamfileoutput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamfileoutput.adoc @@ -31,9 +31,11 @@ The Beam File Output transform writes files using a file definition with the Bea [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:cross.svg[Not Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:cross.svg[Not Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamgcppublisher.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamgcppublisher.adoc index af4f85b8ed7..f178e7ebaff 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamgcppublisher.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamgcppublisher.adoc @@ -32,9 +32,11 @@ The Beam GCP Pub/Sub : Publish transform publishes messages to a link:https://cl [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:cross.svg[Not Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:cross.svg[Not Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamgcpsubscriber.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamgcpsubscriber.adoc index 64408ce01ef..de8fd470c69 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamgcpsubscriber.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamgcpsubscriber.adoc @@ -32,9 +32,11 @@ The Beam GCP Pub/Sub : Subscribe transform gets messages from a link:https://clo [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:cross.svg[Not Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:cross.svg[Not Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamhivecataloginput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamhivecataloginput.adoc index 591c6969b10..a0b360349e6 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamhivecataloginput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamhivecataloginput.adoc @@ -33,9 +33,11 @@ WARNING: This transform is in an experimental state, backwards compatibility bet [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:cross.svg[Not Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:cross.svg[Not Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamkafkaconsume.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamkafkaconsume.adoc index dd2152fae56..c9ee46c2352 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamkafkaconsume.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamkafkaconsume.adoc @@ -32,9 +32,11 @@ The Beam Kafka Consume transform link:https://kafka.apache.org/23/javadoc/index. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:cross.svg[Not Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:cross.svg[Not Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamkafkaproduce.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamkafkaproduce.adoc index 6ecf9e945a8..b4e5b2d6550 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamkafkaproduce.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamkafkaproduce.adoc @@ -31,9 +31,11 @@ The Beam Kafka Produce transform link:https://kafka.apache.org/25/javadoc/index. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:cross.svg[Not Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:cross.svg[Not Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamkinesisconsume.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamkinesisconsume.adoc index aa65bb1d2c7..67fcd18eef9 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamkinesisconsume.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamkinesisconsume.adoc @@ -31,9 +31,11 @@ The Beam link:https://aws.amazon.com/kinesis/[Kinesis] Consume transform consume [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:cross.svg[Not Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:cross.svg[Not Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamkinesisproduce.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamkinesisproduce.adoc index ae9189caf49..6bdb2e6bb6a 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamkinesisproduce.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamkinesisproduce.adoc @@ -31,9 +31,11 @@ The Beam link:https://aws.amazon.com/kinesis/[Kinesis] Produce transform sends d [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:cross.svg[Not Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:cross.svg[Not Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamtimestamp.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamtimestamp.adoc index 585cd235816..8b2c5e7b0ab 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamtimestamp.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamtimestamp.adoc @@ -31,9 +31,11 @@ The Beam Timestamp transform adds a custom timestamp using the Beam execution en [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:cross.svg[Not Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:cross.svg[Not Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamwindow.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamwindow.adoc index 23ae9522b57..6129c3edcc4 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamwindow.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/beamwindow.adoc @@ -31,9 +31,11 @@ The Beam Window transform adds event-time-based window functions using the Beam [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:cross.svg[Not Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:cross.svg[Not Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/blockingtransform.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/blockingtransform.adoc index ee8ccb14ae9..546e0c8733d 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/blockingtransform.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/blockingtransform.adoc @@ -37,9 +37,11 @@ Use the Blocking transform for triggering plugins, stored procedures, Java scrip [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/blockuntiltransformsfinish.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/blockuntiltransformsfinish.adoc index af99b4ca1a7..829773f4772 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/blockuntiltransformsfinish.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/blockuntiltransformsfinish.adoc @@ -34,9 +34,11 @@ You can use it to avoid the natural concurrency (parallelism) that exists betwee [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/calculator.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/calculator.adoc index caa6125ebe2..dd7de1ac044 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/calculator.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/calculator.adoc @@ -37,12 +37,14 @@ TIP: The execution speed of the Calculator is far better than the speed provided | == Supported Engines -[%noheader,cols="2,1a",frame=none,role="table-supported-engines"] +[%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== -!Hop Engine! image:check_mark.svg[Supported,24] -!Spark! image:check_mark.svg[Supported,24] -!Flink! image:check_mark.svg[Supported,24] -!Dataflow! image:check_mark.svg[Supported,24] +!Hop Engine! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/calldbproc.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/calldbproc.adoc index 8388babe0bc..34388808b6b 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/calldbproc.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/calldbproc.adoc @@ -34,9 +34,11 @@ Stored procedures and functions can only return values through their function ar [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/cassandra-input.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/cassandra-input.adoc index 2a730ff4d22..bf20c34f5ed 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/cassandra-input.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/cassandra-input.adoc @@ -34,9 +34,11 @@ The Cassandra Input transform reads data from a Cassandra table of an Apache Cas [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/cassandra-output.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/cassandra-output.adoc index bc9737a7033..72270d2abb7 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/cassandra-output.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/cassandra-output.adoc @@ -34,9 +34,11 @@ The Cassandra Output transform writes data to a Cassandra table of an Apache Cas [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/changefileencoding.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/changefileencoding.adoc index c3f17e4b640..09949328a64 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/changefileencoding.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/changefileencoding.adoc @@ -32,9 +32,11 @@ The Change File Encoding transform changes a text file from one encoding to anot [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/checkfilelocked.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/checkfilelocked.adoc index b9176473518..c8f15d513ed 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/checkfilelocked.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/checkfilelocked.adoc @@ -32,9 +32,11 @@ The Check If A File Is Locked transform tries to determine if a file is locked b [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/checkwebserviceavailable.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/checkwebserviceavailable.adoc index 0b4e6d34d51..3de36000b2e 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/checkwebserviceavailable.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/checkwebserviceavailable.adoc @@ -34,9 +34,11 @@ Further information of the failing reason can be found in the log when debug log [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/clonerow.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/clonerow.adoc index c9f65ba46ea..c4f14044187 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/clonerow.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/clonerow.adoc @@ -32,9 +32,11 @@ The Clone Row transform creates copies (clones) of a row and outputs them direct [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/closure.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/closure.adoc index b9ceb228612..16a73bd2181 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/closure.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/closure.adoc @@ -34,9 +34,11 @@ It attaches the distance (in levels) from parent to child. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/coalesce.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/coalesce.adoc index 2c3010fee77..f271e64eab6 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/coalesce.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/coalesce.adoc @@ -32,9 +32,11 @@ The Coalesce transform lets you list multiple fields and returns the first non-n [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/columnexists.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/columnexists.adoc index 026e44302f3..c9c3c3dc094 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/columnexists.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/columnexists.adoc @@ -32,9 +32,11 @@ The Column Exists transforms allows you to verify the existence of a specific co [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/combinationlookup.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/combinationlookup.adoc index 70a733f013b..51df52a924b 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/combinationlookup.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/combinationlookup.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Combination Lookup-Update transform allows you to store information in a junk-dimension table. The transform can possibly also be used to maintain Kimball pure Type 1 dimensions. +:description: The Combination Lookup-Update transform allows you to store information in a junk-dimension table. = image:transforms/icons/combinationlookup.svg[Combination lookup/update transform Icon, role="image-doc-icon"] Combination lookup/update @@ -49,9 +49,11 @@ This can speed up lookup performance dramatically while limiting the fields to i [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/concatfields.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/concatfields.adoc index f68e890bfaa..6b2a9ae7b54 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/concatfields.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/concatfields.adoc @@ -32,9 +32,11 @@ The Concat Fields transform concatenates multiple fields into one target field. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/copyrowstoresult.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/copyrowstoresult.adoc index 3e1de1325d7..fbc1bfb07b8 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/copyrowstoresult.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/copyrowstoresult.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Copy Rows To Result transform allows you to transfer rows of data (in memory) to the next pipeline or workflow action via an internal result row set. Remember that values or variables will not be copied, only row data. +:description: The Copy Rows To Result transform allows you to transfer rows of data (in memory) to the next pipeline or workflow action. = image:transforms/icons/rowstoresult.svg[Copy rows to result transform Icon, role="image-doc-icon"] Copy rows to result @@ -30,9 +30,11 @@ The Copy Rows To Result transform allows you to transfer rows of data (in memory [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:cross.svg[Not Supported, 24] -!Flink! image:cross.svg[Not Supported, 24] -!Dataflow! image:cross.svg[Not Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/cratedb-bulkloader.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/cratedb-bulkloader.adoc index db3167eedb6..b58bbf0f2fa 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/cratedb-bulkloader.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/cratedb-bulkloader.adoc @@ -36,9 +36,11 @@ The CrateDB Bulk Loader transform loads data from Apache Hop to CrateDB using tw [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/creditcardvalidator.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/creditcardvalidator.adoc index 44a399da15d..683294ad1a7 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/creditcardvalidator.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/creditcardvalidator.adoc @@ -35,9 +35,11 @@ The Credit Card Validator transform will help you check the following: [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/csvinput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/csvinput.adoc index 135f08e8178..9cae15f2a62 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/csvinput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/csvinput.adoc @@ -48,9 +48,11 @@ For information on valid date and numeric formats used in this transform, view t [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:cross.svg[Not Supported, 24] -!Flink! image:cross.svg[Not Supported, 24] -!Dataflow! image:cross.svg[Not Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/data-stream-input.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/data-stream-input.adoc index b6a291b3b00..b2efe91414c 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/data-stream-input.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/data-stream-input.adoc @@ -18,14 +18,29 @@ under the License. :language: en_US :description: The Data Stream Input transform reads from an IPC Data Stream. -= Data Stream Input - -== Overview +[%noheader,cols="3a,1a", role="table-no-borders" ] +|=== +| +== Description The **Data Stream Input** transform reads rows from a configured xref:metadata-types/data-stream/data-stream.adoc[Data Stream] into a Hop pipeline. It supports all available Data Stream implementations (Arrow File Stream, Arrow Random Access File, and Arrow Flight). +| +== Supported Engines +[%noheader,cols="2,1a",frame=none, role="table-supported-engines"] +!=== +!Hop Engine! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] +!=== +|=== + + == Options [cols="1,3"] diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/data-stream-output.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/data-stream-output.adoc index aec06edefa8..4fe11ab2dc3 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/data-stream-output.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/data-stream-output.adoc @@ -18,14 +18,28 @@ under the License. :language: en_US :description: The Data Stream Output transform writes to an IPC Data Stream. -= Data Stream Output - -== Overview +[%noheader,cols="3a,1a", role="table-no-borders" ] +|=== +| +== Description The **Data Stream Output** transform writes rows from a Hop pipeline to a configured xref:/metadata-types/data-stream/data-stream.adoc[Data Stream]. It supports all available Data Stream implementations (Arrow File Stream, Arrow Random Access File, and Arrow Flight). +| +== Supported Engines +[%noheader,cols="2,1a",frame=none, role="table-supported-engines"] +!=== +!Hop Engine! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] +!=== +|=== + == Options [cols="1,3"] diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/databasejoin.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/databasejoin.adoc index f1b11039cc6..ec4358b5385 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/databasejoin.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/databasejoin.adoc @@ -32,9 +32,11 @@ The Database Join transform allows you to run a query against a database using d [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/databaselookup.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/databaselookup.adoc index 5f469aa6f4d..123eedfb1a2 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/databaselookup.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/databaselookup.adoc @@ -32,9 +32,11 @@ Lookup values are added as new fields onto the stream. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/datagrid.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/datagrid.adoc index 7404864a7df..3244f607d26 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/datagrid.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/datagrid.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Data Grid transform allows you to enter a static list of rows in a grid, similar to an in-pipeline spreadsheet. This is usually done for testing, reference or demo purposes. +:description: The Data Grid transform allows you to enter a static list of rows in a grid. = image:transforms/icons/datagrid.svg[Data Grid transform Icon, role="image-doc-icon"] Data Grid @@ -35,9 +35,11 @@ Place a Data grid onto the data orchestration grid. On the Meta tab, add the fie [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/datasetinput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/datasetinput.adoc index e63c66ea31e..54d1250f1e0 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/datasetinput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/datasetinput.adoc @@ -32,9 +32,11 @@ The Data Set Input transform allows you to read from a data set in a pipeline. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dbimpactinput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dbimpactinput.adoc index dc5ba55ec50..ed2553cfeef 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dbimpactinput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dbimpactinput.adoc @@ -20,7 +20,7 @@ under the License. :openvar: ${ :closevar: } -= image:images/icons/database.svg[Database Impact Input, role="image-doc-icon"] Database Impact Input += image:transforms/icons/database.svg[Database Impact Input Icon, role="image-doc-icon"] Database Impact Input [%noheader,cols="3a,1a", role="table-no-borders" ] |=== @@ -37,9 +37,11 @@ IMPORTANT: This transform supports error handling. You can add an error handlin [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/ddl.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/ddl.adoc index f4effbaab1c..fc62b4354e1 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/ddl.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/ddl.adoc @@ -32,9 +32,11 @@ This transform generates https://en.wikipedia.org/wiki/Data_definition_language[ [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/delay.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/delay.adoc index 1266dd74ed8..31b188e7bb9 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/delay.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/delay.adoc @@ -34,9 +34,11 @@ Use this transform if you deliberately want to slow down your pipeline. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/delete.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/delete.adoc index 76041efddfb..ff8dd9ba414 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/delete.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/delete.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Delete transform deletes rows of data from a database. This transform is similar to the update family of transforms in that it takes one or more key fields to determine the rows to delete. +:description: The Delete transform deletes rows of data from a database. = image:transforms/icons/delete.svg[Delete transform Icon, role="image-doc-icon"] Delete @@ -34,9 +34,11 @@ This transform is similar to the update family of transforms in that it takes on [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/detectemptystream.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/detectemptystream.adoc index fa9352cb638..3068d02b743 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/detectemptystream.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/detectemptystream.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Detect Empty Stream transform outputs one single empty row of data if the input stream is empty (ie when input stream does not contain any row). The output row will have the same field layout as the input row, but all field values will be empty (null). +:description: The Detect Empty Stream transform outputs one single empty row of data if the input stream is empty. = image:transforms/icons/detectemptystream.svg[Detect Empty Stream transform Icon, role="image-doc-icon"] Detect Empty Stream @@ -35,9 +35,11 @@ If the input stream is not empty it will not output anything. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/detectlanguage.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/detectlanguage.adoc index 5d5d4226c6a..bad28d8dc42 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/detectlanguage.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/detectlanguage.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Detect Empty Stream transform outputs one single empty row of data if the input stream is empty (ie when input stream does not contain any row). The output row will have the same field layout as the input row, but all field values will be empty (null). +:description: The Detect Language transform examine text to identify the language. = image:transforms/icons/detectlanguage.svg[Detect Language transform Icon, role="image-doc-icon"] Detect Language @@ -34,9 +34,11 @@ https://github.com/pemistahl/lingua?tab=readme-ov-file#3-which-languages-are-sup [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dimensionlookup.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dimensionlookup.adoc index 86b06f7d29e..e12ad4386b6 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dimensionlookup.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dimensionlookup.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Dimension Lookup/Update transform allows you to implement Ralph Kimball's slowly changing dimension for both types: Type I (update) and Type II (insert) together with some additional functions. +:description: The Dimension Lookup/Update transform allows you to implement Ralph Kimball's slowly changing dimensions. = image:transforms/icons/dimensionlookup.svg[Dimension lookup/update transform Icon, role="image-doc-icon"] Dimension lookup/update @@ -34,9 +34,11 @@ This transform can be used to populate a dimension table or to look up values in [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dorisbulkloader.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dorisbulkloader.adoc index 5e48ba265d0..dec9c0a519a 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dorisbulkloader.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dorisbulkloader.adoc @@ -36,9 +36,11 @@ The Doris Bulk Loader allows you to insert data into Apache Doris at high speed [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dummy.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dummy.adoc index 81d0115516f..91c250450ad 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dummy.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dummy.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Dummy transform passes data without making any modifications. It functions as a placeholder for testing purposes or a way to combine multiple streams with the same field layout. +:description: The Dummy transform passes data without making any modifications. = image:transforms/icons/dummy.svg[Dummy transform Icon, role="image-doc-icon"] Dummy (do nothing) @@ -34,9 +34,11 @@ It functions as a placeholder for testing purposes or a way to combine multiple [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dynamicsqlrow.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dynamicsqlrow.adoc index aab67f3c1d0..65a621f5352 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dynamicsqlrow.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/dynamicsqlrow.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Dynamic SQL Row transform allows you to execute a SQL statement that is defined in a database field. The lookup values returned by the transform are added as new fields onto the stream. +:description: The Dynamic SQL Row transform allows you to execute a SQL statement that is defined in a field. :openvar: ${ :closevar: } @@ -37,9 +37,11 @@ The lookup values returned by the transform are added as new fields onto the str [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/edi2xml.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/edi2xml.adoc index 6e06cb30404..d642d51dfde 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/edi2xml.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/edi2xml.adoc @@ -34,9 +34,11 @@ The XML text is more accessible and allows for selective data extraction using X [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/emailinput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/emailinput.adoc index b3509ca0724..c840927521b 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/emailinput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/emailinput.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Email Messages Input transform allows you to retrieve messages and their attachments from a mail server using the POP3, IMAP or MBOX standard protocols. +:description: The Email Messages Input transform allows you to retrieve messages and their attachments from a mail server. = image:transforms/icons/mailinput.svg[Email Messages Input transform Icon, role="image-doc-icon"] Email Messages Input @@ -32,9 +32,11 @@ The Email Messages Input transform allows you to retrieve messages and their att [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/enhancedjsonoutput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/enhancedjsonoutput.adoc index 1768438f93c..7e32cfb88be 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/enhancedjsonoutput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/enhancedjsonoutput.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Enhanced JSON Output transform allows you to generate JSON blocks based on input transform values. Output JSON will be available as a Javascript array or Javascript object depending on transform settings. +:description: The Enhanced JSON Output transform allows you to generate JSON blocks based on input transform values. = image:transforms/icons/JSO.svg[Enhanced JSON Output transform Icon, role="image-doc-icon"] Enhanced JSON Output @@ -36,9 +36,11 @@ TIP: Because this transform loops over the fields defined as Group Key and seria [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/excelinput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/excelinput.adoc index c1fcbdd2433..08f3ee86abd 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/excelinput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/excelinput.adoc @@ -36,9 +36,11 @@ When you read other file types like OpenOffice ODS and using special functions l [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/excelwriter.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/excelwriter.adoc index dc8d709a3ee..92798610076 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/excelwriter.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/excelwriter.adoc @@ -42,9 +42,11 @@ The `.xls` files use a binary format which is better suited for simple content, [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== @@ -53,9 +55,22 @@ The `.xls` files use a binary format which is better suited for simple content, [options="header"] |=== |Format|Extension|Backend|Notes -|Excel 97–2003|`.xls`|POI|Sheet password protection supported -|Excel 2007+|`.xlsx`|POI|Streaming mode for large files; no sheet password protection -|OpenDocument Spreadsheet|`.ods`|ODFDOM|LibreOffice Calc compatible; see <> below + +|Excel 97–2003 +|`.xls` +|POI +|Sheet password protection supported + +|Excel 2007+ +|`.xlsx` +|POI +|Streaming mode for large files; no sheet password protection + +|OpenDocument Spreadsheet +|`.ods` +|ODFDOM +|LibreOffice Calc compatible; see <> below + |=== == Options diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/execinfo.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/execinfo.adoc index 7057f71cd72..00308063d73 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/execinfo.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/execinfo.adoc @@ -35,13 +35,14 @@ This means that this transform always needs input-rows in order to produce outpu First use Generate rows to create your fields and values to use (children: Boolean = true) and (limit: Integer = 200). Then use a first transform to get the ids (Operation: Get execution IDs), and a second connected transform to delete them (Operation: Delete execution). Optionally you can also delete by other fields such as date and ID. It can take a while to query the Execution Information, so keep an eye on the Duration column. | == Supported Engines -[%noheader,cols="2,1a",frame=none,role="table-supported-engines"] - +[%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== -!Hop Engine! image:check_mark.svg[Supported,24] -!Spark! image:check_mark.svg[Maybe Supported,24] -!Flink! image:check_mark.svg[Maybe Supported,24] -!Dataflow! image:check_mark.svg[Maybe Supported,24] +!Hop Engine! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/execprocess.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/execprocess.adoc index aec3d8939c3..f5bec936530 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/execprocess.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/execprocess.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Execute A Process transform executes a shell script (on the host that runs the pipeline). This transform is similar to the Shell workflow action, but can be used in a pipeline to execute for every row. +:description: The Execute A Process transform executes a shell script (on the host that runs the pipeline). = image:transforms/icons/execprocess.svg[Execute a process transform Icon, role="image-doc-icon"] Execute a process @@ -34,9 +34,11 @@ The transform is similar to the xref:workflow/actions/shell.adoc[Shell] workflow [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/execsql.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/execsql.adoc index c4e7006c5fa..f0664ffefa0 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/execsql.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/execsql.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Execute SQL Script transform runs a SQL script either once, during the initialization phase of the pipeline, or once for every input-row that the transform receives and inbound fields are passed on to its output. +:description: The Execute SQL Script transform runs a SQL script. :openvar: ${ :closevar: } @@ -38,9 +38,11 @@ Only in this mode, input fields are passed on to the next hop. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/execsqlrow.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/execsqlrow.adoc index 12df0dd038b..386cf4a2fc4 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/execsqlrow.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/execsqlrow.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Execute Row SQL Script transform executes an SQL script for every input row the transform receives. The SQL to execute can be passed as pipeline fields or read from a file. +:description: The Execute Row SQL Script transform executes an SQL script for every input row the transform receives. = image:transforms/icons/execsqlrow.svg[Execute row SQL script transform Icon, role="image-doc-icon"] Execute row SQL script @@ -34,9 +34,11 @@ The SQL to execute can be passed as pipeline fields or read from a file. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/exectests.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/exectests.adoc index 4ea5bd68ba5..4eee9dbdc29 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/exectests.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/exectests.adoc @@ -32,9 +32,11 @@ The Execute Unit Tests transform fetches and executes the available xref:pipelin [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:cross.svg[Not Supported, 24] -!Flink! image:cross.svg[Not Supported, 24] -!Dataflow! image:cross.svg[Not Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:question_mark.svg[Uncertain, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/fake.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/fake.adoc index 7a4e978697a..032e11cfa2f 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/fake.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/fake.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Fake Data tranform allows you to generate fake data using the Java Faker library. It can be used to generate pretty data for development, testing or showcasing a project. +:description: The Fake Data tranform allows you to generate fake data using the Java Faker library. = image:transforms/icons/fake.svg[Fake data transform Icon, role="image-doc-icon"] Fake data @@ -36,9 +36,11 @@ For instance, we could generate some random Pokémon data. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/fileexists.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/fileexists.adoc index 0214598fcb7..df64de22051 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/fileexists.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/fileexists.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The File Exists transforms verifies the existence of a file. The filenames to check are read from pipeline fields. The transform outputs a boolean flag field, indicating whether a file exists or doesn't. +:description: The File Exists transforms verifies the existence of a file. = image:transforms/icons/fileexists.svg[File exists transform Icon, role="image-doc-icon"] File exists @@ -34,9 +34,11 @@ The transform outputs a boolean flag field, indicating whether a file exists or [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/filemetadata.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/filemetadata.adoc index 460302eba97..4641e677d5a 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/filemetadata.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/filemetadata.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The File Metadata transform scans a file to determine its metadata structure or layout. Use this transforms in situations where you need to read a structured text file (e.g. CSV, TSV) when you don't know the exact layout in advance. +:description: The File Metadata transform scans a file to determine its metadata structure or layout. = image:transforms/icons/filemetadata.svg[File Metadata transform Icon, role="image-doc-icon"] File Metadata @@ -41,9 +41,11 @@ Using 0 rows for 'limit scanned rows' is a way to make sure the entire file is s [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/filesfromresult.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/filesfromresult.adoc index 398a1de470a..329f1125ca2 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/filesfromresult.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/filesfromresult.adoc @@ -34,9 +34,11 @@ Every time a file gets processed, used or created in a pipeline or a workflow, t [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:cross.svg[Not Supported, 24] -!Flink! image:cross.svg[Not Supported, 24] -!Dataflow! image:cross.svg[Not Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/filestoresult.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/filestoresult.adoc index a169feae938..cb82c3ae291 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/filestoresult.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/filestoresult.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Files To Result transform allows you to set filenames in the internal result files of a pipeline, for use by subsequent workflow actions. Subsequent workflow actions can then use this information +:description: The Files To Result transform allows you to set filenames in the internal result files of a pipeline, for use by subsequent workflow actions. = image:transforms/icons/filestoresult.svg[Files to result transform Icon, role="image-doc-icon"] Files to result @@ -40,9 +40,11 @@ WARNING: the Files To Result is available for historical reasons. Check the xref [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:cross.svg[Not Supported, 24] -!Flink! image:cross.svg[Not Supported, 24] -!Dataflow! image:cross.svg[Not Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/filterrows.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/filterrows.adoc index 24e5b27c052..488a04e9877 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/filterrows.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/filterrows.adoc @@ -33,9 +33,11 @@ The Filter Rows transform allows you to filter rows based on conditions and comp [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/formula.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/formula.adoc index 15d23d95190..119e167d3b3 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/formula.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/formula.adoc @@ -45,9 +45,11 @@ With a formula you are not limited to comparing to three fields like in the Calc [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/fuzzymatch.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/fuzzymatch.adoc index 358b82bc7b8..76f19e414bf 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/fuzzymatch.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/fuzzymatch.adoc @@ -34,9 +34,11 @@ This transform returns matching values as a separated list as specified by user- [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/generaterandomvalue.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/generaterandomvalue.adoc index fe3594c6ce3..efc1d1fa7e5 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/generaterandomvalue.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/generaterandomvalue.adoc @@ -32,9 +32,11 @@ The "Generate Random Value" transform generates random numbers, integers, string [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getdatafromxml.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getdatafromxml.adoc index e0252d528bc..39e73f1ea08 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getdatafromxml.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getdatafromxml.adoc @@ -42,9 +42,11 @@ Samples (Samples project): [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== @@ -80,33 +82,62 @@ If a file is required and it is not found, an error is generated;otherwise, the [options="header", cols="1,3"] |=== |Option|Description -2+|Settings -|Loop XPath| For every matching entry in the XML file(s) or data, one row of data is added to the output. +2+|Settings + +|Loop XPath +| For every matching entry in the XML file(s) or data, one row of data is added to the output. This is the main specification used to flatten the XML file(s). You can use the "Get XPath nodes" button to search for the possible repeating nodes in the XML document, however, if the XML document is large, this can take a while. -|Encoding|The XML filename encoding, in case none is specified in the XML documents. -|Namespace aware?|Enable if the XML document requires namespaces to be considered while parsing. -|Ignore comments?|Enable to ignore all comments in the XML document while parsing. -|Validate XML?|Enable to validate the XML prior to parsing. + +|Encoding +|The XML filename encoding, in case none is specified in the XML documents. + +|Namespace aware? +|Enable if the XML document requires namespaces to be considered while parsing. + +|Ignore comments? +|Enable to ignore all comments in the XML document while parsing. + +|Validate XML? +|Enable to validate the XML prior to parsing. Use a token when you want to replace dynamically in a Xpath field value. A token is between @_ and - (@_fieldname-). Please see the Example 1 to see how it works. -|Use token|Enable to use a token for validating the XML. -|Ignore empty file|Enable to ignore any files with no content. These are not valid XML documents. -|Do not raise an error if no files|Enable to do nothing if no files are found. Otherwise, an error is returned. -|Limit|Specify a maximum number of rows to return. Zero (0) returns all rows. -|Prune path to handle large files|Specifies a path, similar to the Loop XPath, used to process chunks of data from the XML file. Each matching value defines a chunk of data that is read and processed. Use the prune path to speed up processing of large files. + +|Use token +|Enable to use a token for validating the XML. + +|Ignore empty file +|Enable to ignore any files with no content. These are not valid XML documents. + +|Do not raise an error if no files +|Enable to do nothing if no files are found. Otherwise, an error is returned. + +|Limit +|Specify a maximum number of rows to return. Zero (0) returns all rows. + +|Prune path to handle large files +|Specifies a path, similar to the Loop XPath, used to process chunks of data from the XML file. Each matching value defines a chunk of data that is read and processed. Use the prune path to speed up processing of large files. You can also use this parameter to avoid multiple HTTP URL requests. You can also do this using the xref:pipeline/transforms/xmlinputstream.adoc[XML Input Stream (StAX)] transform. 2+|Additional fields -|Include filename in output?|Allows you to specify a field name to include the file name (String) in the output of this transform. -|Filename fieldname|The field to read the file name value from. -|Rownum in output?|Allows you to specify a field name to include the row number (Integer) in the output of this transform. -|Rownum fieldname|The field to read the row number value from. +|Include filename in output? +|Allows you to specify a field name to include the file name (String) in the output of this transform. + +|Filename fieldname +|The field to read the file name value from. + +|Rownum in output? +|Allows you to specify a field name to include the row number (Integer) in the output of this transform. + +|Rownum fieldname +|The field to read the row number value from. + +2+|Add to result filename -2+|Add to result filename| -|Add files to result filename|Adds the XML filenames read to the result of this pipeline. A unique list is being kept in memory that can be used in the next workflow action in a workflow, for example in another pipeline. +|Add files to result filename +|Adds the XML filenames read to the result of this pipeline. A unique list is being kept in memory that can be used in the next workflow action in a workflow, for example in another pipeline. |=== @@ -136,7 +167,7 @@ You can also do this using the xref:pipeline/transforms/xmlinputstream.adoc[XML |Select fields from snippet|Click to populate the table with fields corresponding to a Loop Xpath and an XML document that must be provided in the popup dialog. |=== -===Additional output fields tab +=== Additional output fields tab [options="header", cols="1,3"] |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getfilenames.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getfilenames.adoc index 254ab9eb11b..829d596d5af 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getfilenames.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getfilenames.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Get File Names transform allows you to get information associated with file names on the file system. The information about the retrieved files is added as rows onto the stream. +:description: The Get File Names transform allows you to get information associated with file names on the file system. = image:transforms/icons/getfilenames.svg[Get filenames transform Icon, role="image-doc-icon"] Get filenames @@ -32,12 +32,14 @@ The information about the retrieved files is added as rows onto the stream. | == Supported Engines -[%noheader,cols="2,1a",frame=none,role="table-supported-engines"] +[%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== -!Hop Engine! image:check_mark.svg[Supported,24] -!Spark! image:check_mark.svg[Supported,24] -!Flink! image:check_mark.svg[Supported,24] -!Dataflow! image:check_mark.svg[Supported,24] +!Hop Engine! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getfilesrowcount.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getfilesrowcount.adoc index 90826bd501d..c515bd24aff 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getfilesrowcount.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getfilesrowcount.adoc @@ -32,9 +32,11 @@ The Get Files Row Count transform counts the number of rows in a file or set of [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getrecordsfromstream.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getrecordsfromstream.adoc index 73de6c24b42..395228758b8 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getrecordsfromstream.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getrecordsfromstream.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Get Records From Stream transform returns records that were previously generated by another pipeline in a workflow. The records were passed to this transform using either the Copy rows to result transform or the Workflow Executor transform. +:description: The Get Records From Stream transform returns records that were previously generated by another pipeline in a workflow. = image:transforms/icons/recordsfromstream.svg[Get records from stream transform Icon, role="image-doc-icon"] Get records from stream @@ -38,9 +38,11 @@ NOTE: Deprecated use RowsFromResult instead [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getrowsfromresult.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getrowsfromresult.adoc index afafb225d4d..ee4fe8ed88a 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getrowsfromresult.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getrowsfromresult.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Get Rows from Result transform returns rows that were previously generated by another pipeline in a workflow. The rows were passed on to this transform using either the Copy rows to result transform or the Pipeline Executor transform. +:description: The Get Rows from Result transform returns rows that were previously generated by another pipeline in a workflow. = image:transforms/icons/rowsfromresult.svg[Get Rows from Result transform Icon, role="image-doc-icon"] Get Rows from Result @@ -31,9 +31,11 @@ The Get Rows from Result transform returns rows that were previously generated b [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:cross.svg[Not Supported, 24] -!Flink! image:cross.svg[Not Supported, 24] -!Dataflow! image:cross.svg[Not Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getsubfolders.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getsubfolders.adoc index 56683f40a5d..3ea77fa946a 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getsubfolders.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getsubfolders.adoc @@ -35,9 +35,11 @@ Each subfolder produces one output row with metadata fields describing the folde [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getsystemdata.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getsystemdata.adoc index d8b65edfe10..6d8dd2db300 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getsystemdata.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getsystemdata.adoc @@ -37,9 +37,11 @@ It can also accept any number of input streams, aggregate any fields defined by [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/gettablenames.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/gettablenames.adoc index 69919de9356..318cecc90e2 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/gettablenames.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/gettablenames.adoc @@ -31,9 +31,11 @@ The Get Table Names transform gets table names from a database connection. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getvariable.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getvariable.adoc index 8c951b3eab4..39098ae21e0 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getvariable.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/getvariable.adoc @@ -34,9 +34,11 @@ TIP: If you need to refer to a previous pipeline’s data row(s) fields, then u [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/google-analytics.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/google-analytics.adoc index 2f8871ab735..30108fe0575 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/google-analytics.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/google-analytics.adoc @@ -34,9 +34,11 @@ The https://ga-dev-tools.google/ga4/query-explorer/[GA4 Query Explorer] provides [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/google-sheets-input.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/google-sheets-input.adoc index 8a7532468c4..52753725568 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/google-sheets-input.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/google-sheets-input.adoc @@ -34,9 +34,11 @@ This transform requires a Google service account (JSON file) and a Google Cloud [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/google-sheets-output.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/google-sheets-output.adoc index 453d7169898..f5bff3ccc74 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/google-sheets-output.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/google-sheets-output.adoc @@ -34,9 +34,11 @@ This transform requires a Google service account (JSON file) and a Google Cloud [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/groupby.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/groupby.adoc index 2b139e181b8..b7e2e83b59c 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/groupby.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/groupby.adoc @@ -44,9 +44,11 @@ You can use the xref:pipeline/transforms/memgroupby.adoc[Memory Group By] transf [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/html2text.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/html2text.adoc index b41dc7df785..55681cad9f3 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/html2text.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/html2text.adoc @@ -31,8 +31,10 @@ The HTML 2 Text Transform allows you to parse an HTML page and convert the conte [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/http.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/http.adoc index 491b627ce4b..552e66ac633 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/http.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/http.adoc @@ -38,9 +38,11 @@ The result is stored in a String field with the specified name. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/httppost.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/httppost.adoc index 05280d4b81e..19afcba56c6 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/httppost.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/httppost.adoc @@ -32,9 +32,11 @@ The HTTP Post transform uses an HTTP POST command to submit form data via a URL. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/identifylastrow.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/identifylastrow.adoc index 736ee42ac5e..4f76ba0ac4e 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/identifylastrow.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/identifylastrow.adoc @@ -32,9 +32,11 @@ The Identify Last Row In A Stream pipeline transform generates a Boolean field f [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:cross.svg[Not Supported, 24] -!Flink! image:cross.svg[Not Supported, 24] -!Dataflow! image:cross.svg[Not Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/ifnull.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/ifnull.adoc index 4a8c3fd63bd..fa2996d1eae 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/ifnull.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/ifnull.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The If Null transform replaces nulls by a given value either by processing the complete row with all fields, by processing the complete row but only for specific field types (Number, String, Date etc.) or by processing the complete row but only for specific fields by name +:description: The If Null transform replaces nulls by a given value . = image:transforms/icons/ifnull.svg[If Null transform Icon, role="image-doc-icon"] If Null @@ -37,8 +37,10 @@ The If Null transform replaces nulls by a given value either by: [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/injector.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/injector.adoc index 93dd1a9de98..27b02568845 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/injector.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/injector.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Injector transform is used for special purpose pipelines where you want to 'inject' rows into the pipeline using the Hop API and Java, or from streaming input transforms like the Kafka Consumer transform. +:description: The Injector transform is used for special purpose pipelines where you want to 'inject' rows into a pipeline. = image:transforms/icons/injector.svg[Injector transform Icon, role="image-doc-icon"] Injector @@ -34,9 +34,11 @@ Among other things you can build 'headless' pipelines with it: pipelines that ha [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/insertupdate.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/insertupdate.adoc index c206798e1fe..1da68aac89d 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/insertupdate.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/insertupdate.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Insert/Update transform first looks up a row in a table using one or more lookup keys. If a table row for the specified keys can't be found, a new row is inserted. +:description: The Insert/Update transform implements database upsert/merge. = image:transforms/icons/insertupdate.svg[Insert / Update transform Icon, role="image-doc-icon"] Insert / Update @@ -36,9 +36,11 @@ If they are not all the same, the row in the table is updated. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/javafilter.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/javafilter.adoc index 6ac57c5c95c..08b5fed4142 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/javafilter.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/javafilter.adoc @@ -44,9 +44,11 @@ else [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/javascript.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/javascript.adoc index 54516c718e3..18dc9564cfa 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/javascript.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/javascript.adoc @@ -35,9 +35,11 @@ TIP: Variables won’t be usable or testable until you create fields. Click the [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jdbcmetadata.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jdbcmetadata.adoc index a53793fd179..edcb12d1967 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jdbcmetadata.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jdbcmetadata.adoc @@ -33,9 +33,11 @@ The JDBC Metadata transform allows you to fetch metadata from a database connect [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/joinrows.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/joinrows.adoc index b850a073680..9f9dfcf3c03 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/joinrows.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/joinrows.adoc @@ -31,9 +31,11 @@ The Join Rows (cartesian product) transform allows you to combine/join multiple [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jsoninput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jsoninput.adoc index f0202cb5d8b..728cb6aa031 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jsoninput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jsoninput.adoc @@ -54,9 +54,11 @@ Check the `json-input-nested-elements.hpl` sample in the Samples project for an [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== @@ -199,7 +201,9 @@ The Additional output fields tab contains the following options to specify addit While processing input JSON files, if a JSON record has one or more field containing null values, by default the null values will be present in the transform output. For example if we have a JSON file like this -```json + +[source,json] +---- { "persons" : [ { @@ -220,7 +224,7 @@ For example if we have a JSON file like this } ] } -``` +---- When extracting the fields id and Name using the following field definition: [options="header", cols="1a, 3a"] @@ -232,30 +236,33 @@ When extracting the fields id and Name using the following field definition: given the default behavior, the output will be -``` +[source] +---- id;Name 1;Name 1 2;Name 2 3;null 4;Name 4 -``` +---- Now let's only select the `name` field and see what happens -``` +[source] +---- Name Name 1 Name 2 Name 4 -``` +---- You will notice that you only have 3 rowsets returned in this case ( the null line is omited from the result) To change Hop's behavior regarding null values in JSON files, so that null values will not be considered in JSON output, you change the `HOP_JSON_INCLUDE_NULLS` configuration variable and set it's value to N -``` +[source] +---- HOP_JSON_INPUT_INCLUDE_NULLS = N -``` +---- After restaring Hop, when we run the pipeline once again you will have 3 rows resulting because the the null values will be omitted. diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jsonnormalizeinput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jsonnormalizeinput.adoc index 98a60604688..e65b45e4579 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jsonnormalizeinput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jsonnormalizeinput.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The JSON Normalize Input transform reads JSON (files, URLs, or fields), selects an array of records with JsonPath, flattens nested objects into columns, and outputs one row per array element. +:description: The JSON Normalize Input transform. :openvar: ${ :closevar: } @@ -82,7 +82,11 @@ Pipeline unit tests in `integration-tests/json/` (`0008`–`0011`) exercise thes [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Apache Spark, Flink, Dataflow! Not verified for this transform; validate in your environment before relying on distributed execution. +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jsonoutput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jsonoutput.adoc index cdb9f423e24..fa790c2a537 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jsonoutput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jsonoutput.adoc @@ -34,9 +34,11 @@ Output JSON will be available as a javascript array or a javascript object depen [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/kafkaconsumer.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/kafkaconsumer.adoc index a388360e514..828413dc06f 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/kafkaconsumer.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/kafkaconsumer.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Kafka Consumer transform pulls data from Kafka. It runs a sub-pipeline that executes according to message batch size or duration, and can optionally stop when idle. +:description: The Kafka Consumer transform pulls data from a Kafka topic. = image:transforms/icons/KafkaConsumerInput.svg[Kafka Consumer transform Icon, role="image-doc-icon"] Kafka Consumer @@ -51,9 +51,11 @@ For example, you can run the parent pipeline on a timed schedule, drain a topic [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/kafkaproducer.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/kafkaproducer.adoc index e1be10a71d5..fd62a55fd9c 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/kafkaproducer.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/kafkaproducer.adoc @@ -34,9 +34,11 @@ A Kafka Producer transform publishes a stream of records to one Kafka topic. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/languagemodelchat.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/languagemodelchat.adoc index a6b49ce7be9..def32682751 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/languagemodelchat.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/languagemodelchat.adoc @@ -31,12 +31,14 @@ Additionally, the transform supports the application of some prompt engineering | == Supported Engines -[%noheader,cols="2,1a",frame=none,role="table-supported-engines"] +[%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== -!Hop Engine! image:check_mark.svg[Supported,24] -!Spark! image:question_mark.svg[Maybe Supported,24] -!Flink! image:question_mark.svg[Maybe Supported,24] -!Dataflow! image:question_mark.svg[Maybe Supported,24] +!Hop Engine! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/ldapinput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/ldapinput.adoc index 81e45af0b07..ece712583b4 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/ldapinput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/ldapinput.adoc @@ -34,9 +34,11 @@ The following sections describe the available options for the LDAP input transfo [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/ldapoutput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/ldapoutput.adoc index 268dd900676..fe775337839 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/ldapoutput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/ldapoutput.adoc @@ -34,9 +34,11 @@ The following sections describe the available options for the LDAP Output transf [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/loadfileinput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/loadfileinput.adoc index 9e75f36b20a..badc41e223d 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/loadfileinput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/loadfileinput.adoc @@ -32,9 +32,11 @@ The Load File Content In Memory transform loads the content of a file in memory. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mail.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mail.adoc index 5904b55a699..2cdb367a2d9 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mail.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mail.adoc @@ -32,9 +32,11 @@ The Mail transform uses an SMTP server to send an email using data from the pipe [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-input.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-input.adoc index 4e206912141..a05ee8f487a 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-input.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-input.adoc @@ -34,9 +34,11 @@ The data of this input will be provided by the parent pipeline (the pipeline tha [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-output.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-output.adoc index af59fd12dc2..f4338b814e4 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-output.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mapping-output.adoc @@ -34,8 +34,10 @@ The data entering this transform will be streamed to the parent pipeline. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/memgroupby.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/memgroupby.adoc index 515bf64add4..df782843f92 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/memgroupby.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/memgroupby.adoc @@ -37,9 +37,11 @@ TIP: When the number of rows is too large to fit into memory, use a combination [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mergejoin.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mergejoin.adoc index 95ede4e4e69..76350240e91 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mergejoin.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mergejoin.adoc @@ -36,9 +36,11 @@ Join options include INNER, LEFT OUTER, RIGHT OUTER, and FULL OUTER. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mergerows.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mergerows.adoc index 7dd38f33901..ff90515e2fb 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mergerows.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mergerows.adoc @@ -32,9 +32,11 @@ The Merge Rows (Diff) transform compares and merges rows between two data stream [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/metadata-input.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/metadata-input.adoc index 29775ff4514..c8a20d96386 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/metadata-input.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/metadata-input.adoc @@ -34,9 +34,11 @@ It outputs all the metadata objects of all types unless one or more types are fi [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/metainject.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/metainject.adoc index 71545f44a3a..f54a262e10b 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/metainject.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/metainject.adoc @@ -36,9 +36,11 @@ Metadata injection allows Apache Hop users to provide the required metadata for [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:cross.svg[Not Supported, 24] -!Flink! image:cross.svg[Not Supported, 24] -!Dataflow! image:cross.svg[Not Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/metastructure.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/metastructure.adoc index bf69315681e..84c6459a1d0 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/metastructure.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/metastructure.adoc @@ -34,9 +34,11 @@ Before producing this output the transform reads and discards (or eats) all inpu [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/monetdbbulkloader.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/monetdbbulkloader.adoc index f2747fd080e..0caaa6dd315 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/monetdbbulkloader.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/monetdbbulkloader.adoc @@ -32,9 +32,11 @@ The MonetDB Bulk Loader transform bulk loads data to MonetDB. This significantly [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mongodbdelete.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mongodbdelete.adoc index 006a24e1aed..b7d5ae0061e 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mongodbdelete.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mongodbdelete.adoc @@ -34,9 +34,11 @@ For additional information about MongoDB, see the MongoDB http://www.mongodb.org [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mongodbinput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mongodbinput.adoc index 3a9d19e2960..5482dd46782 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mongodbinput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mongodbinput.adoc @@ -34,9 +34,11 @@ For additional information about MongoDB, see the MongoDB http://www.mongodb.org [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mongodboutput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mongodboutput.adoc index 2ace28596b1..9575b5e6166 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mongodboutput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mongodboutput.adoc @@ -34,9 +34,11 @@ For additional information about MongoDB, see the MongoDB http://www.mongodb.org [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/multimerge.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/multimerge.adoc index 7990bc6c242..747f6512437 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/multimerge.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/multimerge.adoc @@ -32,9 +32,11 @@ The Multiway Merge Join transform joins input data from multiple streams. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mysqlbulkloader.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mysqlbulkloader.adoc index 04a94f6f960..82e577088c0 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mysqlbulkloader.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/mysqlbulkloader.adoc @@ -33,9 +33,11 @@ It will create a local file which will then be loaded using the `LOAD DATA` comm [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-cypher.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-cypher.adoc index 8b95068b86c..2247932b3db 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-cypher.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-cypher.adoc @@ -38,9 +38,11 @@ Both reading, writing or both are supported. You can also call procedures and ge [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-gencsv.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-gencsv.adoc index ab264bb05d7..06501575db4 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-gencsv.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-gencsv.adoc @@ -32,9 +32,11 @@ The Neo4j Generate CSV transform writes files for nodes and relationships to the [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-getloginfo.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-getloginfo.adoc index 304c9302332..22556b262fb 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-getloginfo.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-getloginfo.adoc @@ -35,9 +35,11 @@ Check the xref:hop-gui/perspective-neo4j.adoc[Neo4j perspective] for more detail [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-graphoutput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-graphoutput.adoc index 10ff9b0f672..b1d38bb4302 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-graphoutput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-graphoutput.adoc @@ -35,9 +35,11 @@ These will not merge on these nodes, nor will it create or update relationships [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-import.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-import.adoc index a7f87a5888d..c550d845550 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-import.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-import.adoc @@ -44,9 +44,11 @@ when configured appropriately. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-output.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-output.adoc index b05342151b3..66c6edf7506 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-output.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-output.adoc @@ -34,9 +34,11 @@ The transform generates the required Cypher statements with accompanying paramet [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-split-graph.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-split-graph.adoc index 7083aa03d24..f725f4721b7 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-split-graph.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/neo4j-split-graph.adoc @@ -32,9 +32,11 @@ The Neo4j Split Graph transforms splits the nodes and relationships of a graph d [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/nullif.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/nullif.adoc index 0ea2118522c..d78e5c00fe5 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/nullif.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/nullif.adoc @@ -35,8 +35,10 @@ You can add all fields from the input stream(s) using Get Fields. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/numberrange.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/numberrange.adoc index dda29d7c0cb..9fca7790d35 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/numberrange.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/numberrange.adoc @@ -33,9 +33,11 @@ The Number Range transform creates groups numerical values into a number of pred [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/odata-input.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/odata-input.adoc index 55783c0c7e6..3c5eae164d3 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/odata-input.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/odata-input.adoc @@ -34,9 +34,11 @@ This transform performs GET requests against OData Entity Sets, maps JSON proper [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/orabulkloader.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/orabulkloader.adoc index 01c300e82b5..64c3be90a69 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/orabulkloader.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/orabulkloader.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Oracle Bulk Loader transform streams data from Hop to Oracle. It will write the data it receives to a proper load format and will then invoke Oracle SQL*Loader to transfer it to the specified table. +:description: The Oracle Bulk Loader transform streams data from Hop to Oracle. = image:transforms/icons/orabulkloader.svg[Oracle Bulk Loader transform Icon, role="image-doc-icon"] Oracle Bulk Loader @@ -32,9 +32,11 @@ This transform allows you to bulk load data to an Oracle database. It will write [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/parquet-file-input.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/parquet-file-input.adoc index d0b85a0a7c0..e33657f69b2 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/parquet-file-input.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/parquet-file-input.adoc @@ -34,9 +34,11 @@ For more information on this see: http://parquet.apache.org/[Apache Parquet]. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/parquet-file-output.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/parquet-file-output.adoc index 2d8aaac15c5..2850e768549 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/parquet-file-output.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/parquet-file-output.adoc @@ -34,9 +34,11 @@ For more information on this see: http://parquet.apache.org/[Apache Parquet]. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/pgpdecryptstream.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/pgpdecryptstream.adoc index 1c6d5c09b19..04ec0fa76df 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/pgpdecryptstream.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/pgpdecryptstream.adoc @@ -32,9 +32,11 @@ The PGP Decrypt Stream transform decrypts PGP encrypted text. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/pgpencryptstream.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/pgpencryptstream.adoc index 7167878422c..c35ca351329 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/pgpencryptstream.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/pgpencryptstream.adoc @@ -32,9 +32,11 @@ The PGP Encrypt Stream transform encrypts text using PGP. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/pipeline-data-probe.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/pipeline-data-probe.adoc index 3e84a2d8b1c..c51b242dd5d 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/pipeline-data-probe.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/pipeline-data-probe.adoc @@ -34,9 +34,11 @@ Check the xref:metadata-types/pipeline-probe.adoc[Pipeline Probe] metadata type [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/pipeline-executor.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/pipeline-executor.adoc index 98498b79dde..e81c0b66ec9 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/pipeline-executor.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/pipeline-executor.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Pipeline Executor transform allows you to execute a Hop pipeline from within a pipeline. It is similar to the Workflow Executor transform, but works with pipelines. +:description: The Pipeline Executor transform allows you to execute a Hop pipeline from within a pipeline. = image:transforms/icons/pipelineexecutor.svg[Pipeline Executor transform Icon, role="image-doc-icon"] Pipeline Executor @@ -35,9 +35,11 @@ It is similar to the Workflow Executor transform, but works with pipelines. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:cross.svg[Not Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/pipeline-logging.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/pipeline-logging.adoc index 22e197aaee5..f8455c34eab 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/pipeline-logging.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/pipeline-logging.adoc @@ -37,9 +37,11 @@ The transform requires very little configuration, but provides a lot of informat [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/postgresbulkloader.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/postgresbulkloader.adoc index d6eb17e4496..517c665d496 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/postgresbulkloader.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/postgresbulkloader.adoc @@ -34,9 +34,11 @@ TIP: boolean stream fields are serialized as `t` or `f`, which PostgreSQL `COPY` [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/processfiles.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/processfiles.adoc index 823b98a3b01..c79c564d4d5 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/processfiles.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/processfiles.adoc @@ -32,9 +32,11 @@ The Process Files transform copies, moves or deletes files by giving the source [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/propertyinput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/propertyinput.adoc index 1c80c1edb98..690ecce3933 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/propertyinput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/propertyinput.adoc @@ -34,9 +34,11 @@ TIP: More information on this file format is available on https://en.wikipedia.o [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== @@ -47,19 +49,42 @@ TIP: More information on this file format is available on https://en.wikipedia.o [options="header"] |=== |Option|Description -|Transform name|Name of the transform. This name has to be unique in a single pipeline. -|Filename is defined in a field?|The file name to read is available in a field in the input stream. -|Get filename from field|Specify the field to read the file name from. -|File or directory|Specifies the location and/or name of the input text file. -|Add|Click to add the file/directory/wildcard combination to the list of selected files (grid) below.| -|Browse|Click to select the file to load from a file browser. -|Regular expression|Specifies the regular expression you want to use to select the files in the directory specified in the previous option. -|Exclude Regular|Specifies a regular expression to exclude files from the specified directory. -|Selected Files|Contains a list of selected files (or wildcard selections) and a property specifying if that file is required or not. + +|Transform name +|Name of the transform. This name has to be unique in a single pipeline. + +|Filename is defined in a field? +|The file name to read is available in a field in the input stream. + +|Get filename from field +|Specify the field to read the file name from. + +|File or directory +|Specifies the location and/or name of the input text file. +|Add|Click to add the file/directory/wildcard combination to the list of selected files (grid) below. + +|Browse +|Click to select the file to load from a file browser. + +|Regular expression +|Specifies the regular expression you want to use to select the files in the directory specified in the previous option. + +|Exclude Regular +|Specifies a regular expression to exclude files from the specified directory. + +|Selected Files +|Contains a list of selected files (or wildcard selections) and a property specifying if that file is required or not. If a file is required and it is not found, an error is generated, otherwise, the file name is skipped. -|Delete|Click to remove the selected file or wildcard selection from the list. -|Edit|Click to modiy the selected file or wildcard. -|Show filenames(s)...|Displays a list of all files that will be loaded based on the current selected file definitions + +|Delete +|Click to remove the selected file or wildcard selection from the list. + +|Edit +|Click to modiy the selected file or wildcard. + +|Show filenames(s)... +|Displays a list of all files that will be loaded based on the current selected file definitions + |=== === Content Tab @@ -75,7 +100,7 @@ If a file is required and it is not found, an error is generated, otherwise, the |Include filename in output?|Allows you to specify a field name to include the file name in the output of this transform. |Rownum in output|Allows you to specify a field name to include the row number (Integer) in the output of this transform. |Reset rownum per file?|Enable to reset the generated row number to 1 at the start of every individual file. Otherwise, the rownum counts all rows in all files. -|Include section name in output?|Allows you specify a field name to include the section namemin the output of this transform. +|Include section name in output?|Allows you specify a field name to include the section name in the output of this transform. |Add filename to result?|Adds the names of the files read to the result of this pipeline. A unique list is kept in memory that can be used in the next workflow action in a workflow, for example in another pipeline. |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/propertyoutput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/propertyoutput.adoc index 8ebe2313ee5..9763d71ba2e 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/propertyoutput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/propertyoutput.adoc @@ -36,9 +36,11 @@ TIP: More information on this file format is available on https://en.wikipedia.o [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/redshift-bulkloader.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/redshift-bulkloader.adoc index 3e2b9b7e8ab..8f0c7ff1d01 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/redshift-bulkloader.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/redshift-bulkloader.adoc @@ -34,9 +34,11 @@ TIP: make sure your target Redshift table has a layout that is compatible with P [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/regexeval.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/regexeval.adoc index bfa2d78d741..672a0f8c007 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/regexeval.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/regexeval.adoc @@ -38,9 +38,11 @@ You can use this transform to parse a complex string of text and create new fiel [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/repeatfields.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/repeatfields.adoc index eb7b634890a..c66e2547ad4 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/repeatfields.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/repeatfields.adoc @@ -49,9 +49,11 @@ parallel. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:cross.svg[Not Supported, 24] -!Flink! image:cross.svg[Not Supported, 24] -!Dataflow! image:cross.svg[Not Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/replacestring.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/replacestring.adoc index 8954b1c35fb..44379a04d21 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/replacestring.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/replacestring.adoc @@ -39,9 +39,11 @@ You can also use xref::variables.adoc#_hexadecimal_values[hexadecimal replacemen [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/reservoirsampling.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/reservoirsampling.adoc index caf2e3ff937..6d2feae9883 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/reservoirsampling.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/reservoirsampling.adoc @@ -38,9 +38,11 @@ The reservoir sampling transform uses link:https://en.wikipedia.org/wiki/Reservo [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rest.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rest.adoc index 35c7a04765e..583050091dd 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rest.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rest.adoc @@ -45,9 +45,11 @@ The REST client Transform returns a "result" field (can change the name), and th [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rowdenormaliser.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rowdenormaliser.adoc index 02da976ece1..461f25aef70 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rowdenormaliser.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rowdenormaliser.adoc @@ -34,9 +34,11 @@ Note: make sure to check the notes on this transform in the xref:pipeline/beam/g [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rowflattener.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rowflattener.adoc index d4137fb1b70..497f1c76d20 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rowflattener.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rowflattener.adoc @@ -32,9 +32,11 @@ The Row Flattener transform allows you to flatten data sequentially. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rowgenerator.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rowgenerator.adoc index 1b69466657b..0842b66e318 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rowgenerator.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rowgenerator.adoc @@ -41,9 +41,11 @@ Examples: [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rownormaliser.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rownormaliser.adoc index 9bf600c6211..efa6b48b4ee 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rownormaliser.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rownormaliser.adoc @@ -36,9 +36,11 @@ You can use this transform to normalize repeating groups of columns. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rulesaccumulator.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rulesaccumulator.adoc index e31b92c520f..4477f865995 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rulesaccumulator.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rulesaccumulator.adoc @@ -34,9 +34,11 @@ https://www.drools.org/[Drools] is the present rule engine implementation and it [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rulesexecutor.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rulesexecutor.adoc index 9ea2ea5937a..e98194d0de5 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rulesexecutor.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/rulesexecutor.adoc @@ -34,9 +34,11 @@ https://www.drools.org/[Drools] is the present rule engine implementation and it [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/runssh.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/runssh.adoc index b17c67cb17e..32c8803b6ee 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/runssh.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/runssh.adoc @@ -34,9 +34,11 @@ You can pass text to stdout or stderr in the commands. This information can then [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/salesforcedelete.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/salesforcedelete.adoc index 830f69d1b68..81464bf41f5 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/salesforcedelete.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/salesforcedelete.adoc @@ -32,9 +32,11 @@ The Salesforce Delete transform deletes records directly from your Salesforce da [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/salesforceinput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/salesforceinput.adoc index 35273caf421..ac0825985ee 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/salesforceinput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/salesforceinput.adoc @@ -38,9 +38,11 @@ You can also use the following transforms for various ways to modify your Salesf [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/salesforceinsert.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/salesforceinsert.adoc index 7995f35922f..5001ffeb214 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/salesforceinsert.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/salesforceinsert.adoc @@ -39,9 +39,11 @@ You can also use the following other transforms to modify your Salesforce databa [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/salesforceupdate.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/salesforceupdate.adoc index d55b1069044..5a8866e20bb 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/salesforceupdate.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/salesforceupdate.adoc @@ -39,9 +39,11 @@ You can also use the following other transforms to modify your Salesforce databa [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/salesforceupsert.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/salesforceupsert.adoc index f8de7a787fb..6c71778c76b 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/salesforceupsert.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/salesforceupsert.adoc @@ -39,9 +39,11 @@ You can also use the following other transforms to modify your Salesforce databa [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/samplerows.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/samplerows.adoc index db800f06ab9..ba4aa081a58 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/samplerows.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/samplerows.adoc @@ -32,9 +32,11 @@ The Sample Rows transform retains a sample set of rows. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sasinput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sasinput.adoc index 9caf597cd19..f7c829b7a80 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sasinput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sasinput.adoc @@ -34,9 +34,11 @@ The functionality is backed by the https://github.com/epam/parso[Parso java libr [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/schemamapping.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/schemamapping.adoc index 2d39fd80546..24090fa073b 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/schemamapping.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/schemamapping.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Schema Mapping transform maps fields from an incoming stream over a specified Schema Definition. The result of this transform is an output stream that conforms to the mapped Schema Definition. +:description: The Schema Mapping transform maps fields from an incoming stream over a specified Schema Definition. = image:transforms/icons/schemamapping.svg[Schema Mapping transform Icon, role="image-doc-icon"] Schema Mapping @@ -32,9 +32,11 @@ The Schema Mapping transform maps fields from an incoming stream over a specifie [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/script.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/script.adoc index ed722735408..a6cd5e159d5 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/script.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/script.adoc @@ -41,9 +41,11 @@ On Java 17 and later, the JVM no longer includes an ECMAScript engine; Hop bundl [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/selectvalues.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/selectvalues.adoc index 39ccccb7868..ff6c85e05ae 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/selectvalues.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/selectvalues.adoc @@ -38,9 +38,11 @@ These operations are organized into different categories: [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/serialize-de-from-file.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/serialize-de-from-file.adoc index cd9b499cd90..6add72be2e5 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/serialize-de-from-file.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/serialize-de-from-file.adoc @@ -32,9 +32,11 @@ The De-serialize From File transform reads rows of data from a binary Hop file c [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/serialize-to-file.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/serialize-to-file.adoc index 1e4bf179daa..c73ca4f1279 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/serialize-to-file.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/serialize-to-file.adoc @@ -36,8 +36,10 @@ The Serialize to file transform supports a write-once access pattern, and does n [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/serverstatus.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/serverstatus.adoc index 5adf599cc2c..207007c5951 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/serverstatus.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/serverstatus.adoc @@ -35,9 +35,11 @@ Every output field for which you specify a name will be added to the list of out [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/setvalueconstant.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/setvalueconstant.adoc index 25eabdbbb4e..1019f07de99 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/setvalueconstant.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/setvalueconstant.adoc @@ -32,9 +32,11 @@ The Set Field Value To A Constant transform replaces the value of a field with a [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/setvaluefield.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/setvaluefield.adoc index 57c99bf5736..65339e5d36d 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/setvaluefield.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/setvaluefield.adoc @@ -32,9 +32,11 @@ The Set Field Value transform replaces the value of a field with the value of an [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/setvariable.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/setvariable.adoc index 18256d9104c..5abd78ce015 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/setvariable.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/setvariable.adoc @@ -30,9 +30,11 @@ Set Variables allows you to set variables from fields. By clicking the Get Field [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/simple-mapping.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/simple-mapping.adoc index d1f45c0eaa5..606cf16ab68 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/simple-mapping.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/simple-mapping.adoc @@ -31,9 +31,11 @@ The Simple Mapping transform allows you to re-use a series of transforms in the [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/snowflakebulkloader.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/snowflakebulkloader.adoc index ee35c19dd7b..9f78bae37f4 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/snowflakebulkloader.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/snowflakebulkloader.adoc @@ -16,7 +16,7 @@ under the License. //// :documentationPath: /pipeline/transforms/ :language: en_US -:description: The Snowflake Bulk Loader transform utilizes the Snowflake Copy command to load data as opposed to sending individual insert statements through the Table Output transform +:description: The Snowflake Bulk Loader transform utilizes the Snowflake Copy command to load data. = image:transforms/icons/snowflakebulkloader.svg[Snowflake Bulk Loader transform Icon, role="image-doc-icon"] Snowflake Bulk Loader @@ -36,9 +36,11 @@ The Snowflake Bulk Loader transform utilizes the Snowflake Copy command to load [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/snowflakeid.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/snowflakeid.adoc index f14ab5dde90..c464ce19864 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/snowflakeid.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/snowflakeid.adoc @@ -40,9 +40,11 @@ Typical use cases: [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sort.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sort.adoc index f23ec53a7bf..fe1eedf6fb9 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sort.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sort.adoc @@ -36,9 +36,11 @@ TIP: You use can use multiple copies of the Sort Rows transform to speed up larg [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:cross.svg[Not Supported, 24] -!Flink! image:cross.svg[Not Supported, 24] -!Dataflow! image:cross.svg[Not Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sortedmerge.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sortedmerge.adoc index 2b7b98d78a3..eaa2756a13b 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sortedmerge.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sortedmerge.adoc @@ -32,9 +32,11 @@ The Sorted Merge transform merges rows coming from multiple input transforms pro [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:cross.svg[Not Supported, 24] -!Flink! image:cross.svg[Not Supported, 24] -!Dataflow! image:cross.svg[Not Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sortedschemamerge.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sortedschemamerge.adoc index bfbd09c0e21..78b1f2f6a23 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sortedschemamerge.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sortedschemamerge.adoc @@ -40,9 +40,11 @@ IMPORTANT: Each input stream must already be sorted on the configured sort keys [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:cross.svg[Not Supported, 24] -!Flink! image:cross.svg[Not Supported, 24] -!Dataflow! image:cross.svg[Not Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/spark-lake-table-input.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/spark-lake-table-input.adoc new file mode 100644 index 00000000000..4791a6b7d4a --- /dev/null +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/spark-lake-table-input.adoc @@ -0,0 +1,130 @@ +//// +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. +//// +:documentationPath: /pipeline/transforms/ +:language: en_US +:description: Spark Lake Table Input reads Delta Lake or Apache Iceberg tables on the native Spark pipeline engine, with optional time travel and projection. + += image:transforms/icons/spark-lake-table-input.svg[Spark Lake Table Input transform Icon, role="image-doc-icon"] Spark Lake Table Input + +[%noheader,cols="3a,1a", role="table-no-borders" ] +|=== +| +== Description + +*Spark Lake Table Input* reads an open table format table (https://delta.io/[Delta Lake] or https://iceberg.apache.org/[Apache Iceberg]) as a Spark SQL `Dataset` on the xref:pipeline/spark/getting-started-with-native-spark.adoc[native Spark pipeline engine]. + +It supports **PATH** and **TABLE** identifiers, optional field projection, and **time travel** by version/snapshot or timestamp. + +This transform is **not** available on the local Hop engine or on Beam engines. Connector JARs ship with the native Spark plugin; see xref:pipeline/spark/lakehouse.adoc[Lakehouse tables on the native Spark engine]. + +| +== Supported Engines +[%noheader,cols="2,1a",frame=none, role="table-supported-engines"] +!=== +!Hop Engine! image:cross.svg[Not Supported, 24] +!Single Threaded! image:cross.svg[Not Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:cross.svg[Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] +!=== + +NOTE: *Spark* here means Hop’s **native** Spark pipeline engine only (not Beam Spark). +|=== + +== Options + +[options="header",cols="1,3"] +|=== +|Option |Description + +|Transform name +|Unique name of the transform in the pipeline. + +|Format +|`delta` or `iceberg`. + +|Identifier mode +|`PATH` — table root path/URI. + +`TABLE` — catalog-qualified name (e.g. `lake.db.orders`). + +|Table path +|Used when mode is PATH. Must be visible to Spark workers (local path on `local[*]`; shared URI on cluster). + +|Table identifier +|Used when mode is TABLE. Multi-part name: `catalog.namespace.table`. + +|Spark Catalog metadata name +|Optional Hop xref:metadata-types/spark-catalog.adoc[Spark Catalog] entry to register for TABLE mode (Iceberg Hadoop/REST, …). Leave empty if the catalog is already configured on the session. + +|Time travel type +|`NONE` (current), `VERSION`, or `TIMESTAMP`. + +|Time travel version +|Delta table version number or Iceberg snapshot id (when type is VERSION). Supports variables. + +|Time travel timestamp +|Spark-parseable timestamp string when type is TIMESTAMP, e.g. `2024-01-15 10:30:00`. + +|Extra options +|Optional Spark read options as `key=value` lines (advanced). + +|Fields +|Optional projection. When empty, all columns are returned. When set, columns are selected/cast by name (same idea as Spark File Input with a schema). +|=== + +== Time travel + +[options="header",cols="1,2,2"] +|=== +|Type |Delta |Iceberg + +|NONE +|Current table version +|Current snapshot + +|VERSION +|Read option `versionAsOf` +|SQL `VERSION AS OF ` + +|TIMESTAMP +|Read option `timestampAsOf` +|SQL `TIMESTAMP AS OF TIMESTAMP '…'` +|=== + +Tip: After two Overwrite writes, Delta version `0` is typically the first commit. For Iceberg, inspect `{table}.snapshots` (or catalog metadata) for snapshot ids. + +== Metadata injection + +All options support xref:pipeline/metadata-injection.adoc[metadata injection] (ETL Metadata Injection). + +Common keys: `FORMAT`, `IDENTIFIER_MODE`, `TABLE_PATH`, `TABLE_IDENTIFIER`, `CATALOG_METADATA_NAME`, `TIME_TRAVEL_TYPE`, `TIME_TRAVEL_VERSION`, `TIME_TRAVEL_TIMESTAMP`, `EXTRA_OPTIONS`. + +Field projection group `FIELDS`: `NAME`, `TYPE`, `LENGTH`, `PRECISION`, `FORMAT_MASK`. + +Typical pattern: template pipeline with Lake Table Input; injecting pipeline supplies table paths / identifiers (and optional field list) per supplier or table. + +== Notes + +* Prefer Lake Table Input over *Spark File Input* for `delta` / `iceberg` so session extensions, `hop_iceberg` PATH catalog, and probes stay consistent. +* See the xref:pipeline/spark/lakehouse.adoc[lakehouse guide] for packaging, PATH vs TABLE, and troubleshooting. + +== Related pages + +* xref:pipeline/spark/lakehouse.adoc[Lakehouse tables on the native Spark engine] +* xref:pipeline/transforms/spark-lake-table-output.adoc[Spark Lake Table Output] +* xref:metadata-types/spark-catalog.adoc[Spark Catalog] diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/spark-lake-table-maintenance.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/spark-lake-table-maintenance.adoc new file mode 100644 index 00000000000..82dbc8a23b4 --- /dev/null +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/spark-lake-table-maintenance.adoc @@ -0,0 +1,140 @@ +//// +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. +//// +:documentationPath: /pipeline/transforms/ +:language: en_US +:description: Spark Lake Table Maintenance runs OPTIMIZE, VACUUM, expire snapshots, and related operations on Delta Lake or Apache Iceberg tables. + += image:transforms/icons/spark-lake-table-maintenance.svg[Spark Lake Table Maintenance transform Icon, role="image-doc-icon"] Spark Lake Table Maintenance + +[%noheader,cols="3a,1a", role="table-no-borders" ] +|=== +| +== Description + +*Spark Lake Table Maintenance* runs operational SQL / procedures against a https://delta.io/[Delta Lake] or https://iceberg.apache.org/[Apache Iceberg] table on the xref:pipeline/spark/getting-started-with-native-spark.adoc[native Spark pipeline engine]: compaction (OPTIMIZE / rewrite data files), vacuum, expire snapshots, rewrite manifests, or conditional deletes. + +It is a **zero-input** action sink (no hop required). Destructive operations require an explicit acknowledgement. + +Connectors ship with the native Spark plugin. Session rules: xref:pipeline/spark/lakehouse.adoc[Lakehouse tables on the native Spark engine]. + +| +== Supported Engines +[%noheader,cols="2,1a",frame=none, role="table-supported-engines"] +!=== +!Hop Engine! image:cross.svg[Not Supported, 24] +!Single Threaded! image:cross.svg[Not Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] +!=== + +NOTE: *Spark* here means Hop’s **native** Spark pipeline engine only (not Beam Spark). +|=== + +== Options + +[options="header",cols="1,3"] +|=== +|Option |Description + +|Transform name +|Unique name of the transform in the pipeline. + +|Format +|`delta` or `iceberg`. + +|Identifier mode +|`PATH` or `TABLE` for the target table. + +|Table path / Table identifier / Spark Catalog +|Same semantics as other Lake Table transforms. + +|Operation +|See <> below. + +|Retention hours +|**Required** for `VACUUM` and `EXPIRE_SNAPSHOTS`. No silent default — you must set how long to retain history. + +|Retain last +|Iceberg `expire_snapshots` `retain_last` (defaults to `1` when retention is set and this field is blank). + +|Where clause +|Optional filter for `OPTIMIZE` / required for `DELETE_WHERE` (without the `WHERE` keyword), e.g. `id < 10`. + +|Z-Order columns +|Optional comma-separated columns for Delta `OPTIMIZE … ZORDER BY (…)`. + +|Acknowledge destructive +|Must be enabled for `VACUUM`, `EXPIRE_SNAPSHOTS`, and `DELETE_WHERE`. Confirms that data files or snapshots may be removed. +|=== + +[[operations]] +== Operations matrix + +[options="header",cols="1,2,2"] +|=== +|Operation |Delta |Iceberg + +|OPTIMIZE +|`OPTIMIZE delta.\`path\` [ZORDER BY …]` (optional WHERE) +|`CALL cat.system.rewrite_data_files(table => '…')` + +|VACUUM +|`VACUUM … RETAIN n HOURS` (**n required**); destructive ack +|Not used — use `EXPIRE_SNAPSHOTS` + +|EXPIRE_SNAPSHOTS +|n/a +|`CALL … expire_snapshots(…)` with retention / retain_last; destructive ack + +|REWRITE_MANIFESTS +|n/a +|`CALL … rewrite_manifests(…)` + +|DELETE_WHERE +|`DELETE FROM … WHERE …` (**WHERE required**); destructive ack +|Same SQL pattern; destructive ack +|=== + +Aliases accepted for operation: `COMPACT` / `REWRITE_DATA_FILES` → OPTIMIZE; `EXPIRE` → EXPIRE_SNAPSHOTS; `DELETE` → DELETE_WHERE. + +== Behaviour + +* SQL is executed as a Spark action during graph materialisation; an empty leaf Dataset is registered afterward. +* Iceberg CALL procedures use the catalog name from TABLE mode (`lake` in `lake.db.t`) or `hop_iceberg` for PATH mode. +* Metrics are **log-only**; expect little or no GUI row traffic on this sink. + +== Metadata injection + +Supports xref:pipeline/metadata-injection.adoc[metadata injection]. + +Keys: `FORMAT`, `IDENTIFIER_MODE`, `TABLE_PATH`, `TABLE_IDENTIFIER`, `CATALOG_METADATA_NAME`, `OPERATION`, `RETENTION_HOURS`, `RETAIN_LAST`, `WHERE_CLAUSE`, `ZORDER_COLUMNS`, `ACKNOWLEDGE_DESTRUCTIVE`. + +Injecting pipelines can drive fleet-wide OPTIMIZE/VACUUM across many tables; still set retention and destructive acknowledgement deliberately. + +== Safety + +* There is **no** default retention for vacuum/expire — set hours deliberately for your recovery needs. +* Destructive ops without acknowledgement fail validation. +* Prefer OPTIMIZE / rewrite for routine compaction; use VACUUM / expire only when you understand snapshot and time-travel impact. + +== Related pages + +* xref:pipeline/spark/lakehouse.adoc[Lakehouse tables on the native Spark engine] +* xref:pipeline/transforms/spark-lake-table-output.adoc[Spark Lake Table Output] +* xref:pipeline/transforms/spark-lake-table-merge.adoc[Spark Lake Table Merge] diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/spark-lake-table-merge.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/spark-lake-table-merge.adoc new file mode 100644 index 00000000000..72713919c81 --- /dev/null +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/spark-lake-table-merge.adoc @@ -0,0 +1,119 @@ +//// +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. +//// +:documentationPath: /pipeline/transforms/ +:language: en_US +:description: Spark Lake Table Merge runs MERGE INTO against Delta Lake or Apache Iceberg tables on the native Spark pipeline engine. + += image:transforms/icons/spark-lake-table-merge.svg[Spark Lake Table Merge transform Icon, role="image-doc-icon"] Spark Lake Table Merge + +[%noheader,cols="3a,1a", role="table-no-borders" ] +|=== +| +== Description + +*Spark Lake Table Merge* applies a Spark SQL `MERGE INTO` against a https://delta.io/[Delta Lake] or https://iceberg.apache.org/[Apache Iceberg] target table on the xref:pipeline/spark/getting-started-with-native-spark.adoc[native Spark pipeline engine]. + +The **single main-stream upstream hop** is the merge **source**. The transform builds (or accepts) MERGE SQL, executes it as a Spark action, and registers an empty leaf Dataset. + +Connectors ship with the native Spark plugin. Session rules: xref:pipeline/spark/lakehouse.adoc[Lakehouse tables on the native Spark engine]. + +| +== Supported Engines +[%noheader,cols="2,1a",frame=none, role="table-supported-engines"] +!=== +!Hop Engine! image:cross.svg[Not Supported, 24] +!Single Threaded! image:cross.svg[Not Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] +!=== + +NOTE: *Spark* here means Hop’s **native** Spark pipeline engine only (not Beam Spark). +|=== + +== Options + +[options="header",cols="1,3"] +|=== +|Option |Description + +|Transform name +|Unique name of the transform in the pipeline. + +|Format +|`delta` or `iceberg`. + +|Identifier mode +|`PATH` or `TABLE` for the **target** table. + +|Table path / Table identifier / Spark Catalog +|Same semantics as Lake Table Input/Output for the merge target. + +|Merge condition +|`ON` clause body. Use aliases **`t`** (target) and **`s`** (source), e.g. `t.id = s.id`. Required unless raw SQL is set. + +|When matched +|`UPDATE_ALL` — `WHEN MATCHED THEN UPDATE SET *` + +`DELETE` — delete matched target rows + +`NONE` — no matched clause + +|When not matched +|`INSERT_ALL` — `WHEN NOT MATCHED THEN INSERT *` + +`NONE` — no not-matched clause + +|When not matched by source +|`NONE` (default) or `DELETE` (Delta; Iceberg support depends on engine/version). Removes target rows with no source match. + +|Raw MERGE SQL +|Advanced override. When non-empty, replaces structured fields. Empty by default. Operator is trusted to supply valid SQL. +|=== + +== Behaviour + +* Source rows are registered as a temporary view; structured mode emits SQL similar to: ++ +---- +MERGE INTO delta.`/path/to/table` AS t +USING hop_merge_src_… AS s +ON t.id = s.id +WHEN MATCHED THEN UPDATE SET * +WHEN NOT MATCHED THEN INSERT * +---- +* Target SQL ids: Delta PATH → `delta.\`path\``; Iceberg PATH → `hop_iceberg.\`uri\``; TABLE → multi-part identifier. +* At least one action (matched and/or not-matched) is required. +* Metrics are **log-only** (merge completion); GUI in/out row counts for the sink may stay at zero. + +== Metadata injection + +Supports xref:pipeline/metadata-injection.adoc[metadata injection]. + +Keys: `FORMAT`, `IDENTIFIER_MODE`, `TABLE_PATH`, `TABLE_IDENTIFIER`, `CATALOG_METADATA_NAME`, `MERGE_CONDITION`, `MATCHED_ACTION`, `NOT_MATCHED_ACTION`, `NOT_MATCHED_BY_SOURCE_ACTION`, `RAW_MERGE_SQL`. + +Useful for multi-table CDC/upsert templates where merge keys and target tables vary per run. + +== Notes + +* Exactly **one** main-stream input hop. +* Prefer structured fields for common upserts; use raw SQL only when you need full MERGE control. +* Packaging and PATH vs TABLE: xref:pipeline/spark/lakehouse.adoc[lakehouse guide]. + +== Related pages + +* xref:pipeline/spark/lakehouse.adoc[Lakehouse tables on the native Spark engine] +* xref:pipeline/transforms/spark-lake-table-output.adoc[Spark Lake Table Output] +* xref:pipeline/transforms/spark-lake-table-input.adoc[Spark Lake Table Input] diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/spark-lake-table-output.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/spark-lake-table-output.adoc new file mode 100644 index 00000000000..a939b2d62b6 --- /dev/null +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/spark-lake-table-output.adoc @@ -0,0 +1,113 @@ +//// +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. +//// +:documentationPath: /pipeline/transforms/ +:language: en_US +:description: Spark Lake Table Output writes Delta Lake or Apache Iceberg tables on the native Spark pipeline engine with ACID commit semantics. + += image:transforms/icons/spark-lake-table-output.svg[Spark Lake Table Output transform Icon, role="image-doc-icon"] Spark Lake Table Output + +[%noheader,cols="3a,1a", role="table-no-borders" ] +|=== +| +== Description + +*Spark Lake Table Output* writes the incoming Spark `Dataset` to a https://delta.io/[Delta Lake] or https://iceberg.apache.org/[Apache Iceberg] table using table-format commit semantics on the xref:pipeline/spark/getting-started-with-native-spark.adoc[native Spark pipeline engine]. + +Unlike *Spark File Output* (classic file formats), the **default save mode is ErrorIfExists** so ACID tables are not overwritten by accident. + +This transform is **native Spark only**. Connectors ship with the native Spark plugin; see xref:pipeline/spark/lakehouse.adoc[Lakehouse tables on the native Spark engine]. + +| +== Supported Engines +[%noheader,cols="2,1a",frame=none, role="table-supported-engines"] +!=== +!Hop Engine! image:cross.svg[Not Supported, 24] +!Single Threaded! image:cross.svg[Not Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] +!=== + +NOTE: *Spark* here means Hop’s **native** Spark pipeline engine only (not Beam Spark). +|=== + +== Options + +[options="header",cols="1,3"] +|=== +|Option |Description + +|Transform name +|Unique name of the transform in the pipeline. + +|Format +|`delta` or `iceberg`. + +|Identifier mode +|`PATH` or `TABLE` (see xref:pipeline/spark/lakehouse.adoc#path-vs-table[PATH vs TABLE]). + +|Table path +|PATH mode: table root directory / URI. + +|Table identifier +|TABLE mode: e.g. `lake.db.orders`. + +|Spark Catalog metadata name +|Optional xref:metadata-types/spark-catalog.adoc[Spark Catalog] Hop metadata for TABLE mode. + +|Save mode +|`ErrorIfExists` (**default**), `Append`, `Overwrite`, or `Ignore`. Choose Overwrite or Append explicitly when you intend to replace or extend data. + +|Partition by +|Optional comma-separated partition column list. + +|Coalesce +|Optional partition count before write (e.g. `1` for a single file layout — costs a shuffle). + +|Extra options +|Optional Spark write options as `key=value` lines (advanced). +|=== + +== Behaviour + +* The write runs as a Spark **action** when the engine materialises the graph. +* An empty leaf Dataset is registered so a later engine-wide `count()` does not re-write the table. +* **Delta PATH:** `format("delta").mode(…).save(path)` (requires Delta extension + `DeltaCatalog` on the session — hop-run applies these when Delta transforms are present). +* **Iceberg PATH:** `writeTo(hop_iceberg.\`uri\`).using("iceberg")` with the built-in `hop_iceberg` Hadoop catalog. +* **Iceberg TABLE:** `writeTo(catalog.ns.table).using("iceberg")`. +* **Delta TABLE (advanced):** `format("delta").saveAsTable(id)`. + +== Metadata injection + +Supports xref:pipeline/metadata-injection.adoc[metadata injection]. + +Keys: `FORMAT`, `IDENTIFIER_MODE`, `TABLE_PATH`, `TABLE_IDENTIFIER`, `CATALOG_METADATA_NAME`, `SAVE_MODE`, `PARTITION_BY`, `COALESCE`, `EXTRA_OPTIONS`. + +Use this with a template write path when landing many tables (different paths/identifiers/save modes) from one injecting pipeline. + +== Notes + +* Requires exactly one main-stream upstream hop (the data to write). +* For upserts, use xref:pipeline/transforms/spark-lake-table-merge.adoc[Spark Lake Table Merge] instead of Overwrite. +* Packaging, session conf, and troubleshooting: xref:pipeline/spark/lakehouse.adoc[lakehouse guide]. + +== Related pages + +* xref:pipeline/spark/lakehouse.adoc[Lakehouse tables on the native Spark engine] +* xref:pipeline/transforms/spark-lake-table-input.adoc[Spark Lake Table Input] +* xref:pipeline/transforms/spark-lake-table-merge.adoc[Spark Lake Table Merge] diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/splitfields.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/splitfields.adoc index d3aafabfb7a..62384e15eec 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/splitfields.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/splitfields.adoc @@ -32,9 +32,11 @@ The Split Fields transform splits a field into multiple fields based on a specif [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/splitfieldtorows.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/splitfieldtorows.adoc index 88c03ee916a..140d42e20c0 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/splitfieldtorows.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/splitfieldtorows.adoc @@ -32,9 +32,11 @@ The Split Fields To Rows transform splits a row containing a delimited field int [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/splunkinput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/splunkinput.adoc index b8e317fa599..7b419435968 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/splunkinput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/splunkinput.adoc @@ -34,9 +34,11 @@ Check the xref:metadata-types/splunk-connection.adoc[Splunk Connection docs] for [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sqlfileoutput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sqlfileoutput.adoc index 1e1d3479fa7..e3123377511 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sqlfileoutput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sqlfileoutput.adoc @@ -34,9 +34,11 @@ The SQL is generated in the dialect of the selected database. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sstable-output.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sstable-output.adoc index 011bd3a5b38..8f45463b1a5 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sstable-output.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/sstable-output.adoc @@ -32,9 +32,11 @@ The Cassandra SSTable Output transform writes to a file system directory as a Ca [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/standardizephonenumber.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/standardizephonenumber.adoc index e70259d20c9..f07f15f0d9e 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/standardizephonenumber.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/standardizephonenumber.adoc @@ -34,9 +34,11 @@ The transform uses https://github.com/google/libphonenumber[Google libphonenumbe [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/stanfordnlp.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/stanfordnlp.adoc index 742a84c47c4..2ebe90a68ad 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/stanfordnlp.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/stanfordnlp.adoc @@ -31,9 +31,11 @@ Stanford CoreNLP provides a set of natural language analysis tools which can tak [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/streamlookup.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/streamlookup.adoc index 18f611c36f0..624e1dd7bf0 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/streamlookup.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/streamlookup.adoc @@ -36,9 +36,11 @@ TIP: Since this transform loads the lookup data into memory, it can be an extrem [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/streamschemamerge.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/streamschemamerge.adoc index f9fa84622e9..032ff305813 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/streamschemamerge.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/streamschemamerge.adoc @@ -35,9 +35,11 @@ If you want to make sure fields keep their original data type, make sure to conv [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/stringcut.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/stringcut.adoc index 3da4961ad22..5cf5ab73188 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/stringcut.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/stringcut.adoc @@ -32,9 +32,11 @@ The Strings Cut transform cuts a portion of a string (i.e., a substring). If the [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/stringoperations.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/stringoperations.adoc index c4aef185b04..d89f1f5fc04 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/stringoperations.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/stringoperations.adoc @@ -40,9 +40,11 @@ The String Operations transform can perform the following string operations on a [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/switchcase.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/switchcase.adoc index c37590e6c5f..e1211434c55 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/switchcase.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/switchcase.adoc @@ -34,9 +34,11 @@ In our case we route rows of data to one or more target transforms based on the [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/synchronizeaftermerge.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/synchronizeaftermerge.adoc index 656f9b04d05..efe42e6f522 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/synchronizeaftermerge.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/synchronizeaftermerge.adoc @@ -36,9 +36,11 @@ This flag column is then used by the Synchronize After Merge pipeline transform [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tablecompare.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tablecompare.adoc index ca686f86891..4c9d08c5e73 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tablecompare.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tablecompare.adoc @@ -37,9 +37,11 @@ TIP: You can click on the red error handling arrow to configure additional infor [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tableexists.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tableexists.adoc index 9c78d0ca338..cbd34c74a17 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tableexists.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tableexists.adoc @@ -32,9 +32,11 @@ The Table Exists transform verifies if a certain table exists in a database. The [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tableinput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tableinput.adoc index 62a7d14b47e..28fed70917c 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tableinput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tableinput.adoc @@ -40,9 +40,11 @@ These features make Table Input ideal for dynamic and reusable data retrieval lo [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tableoutput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tableoutput.adoc index 34d840a05ab..86651745e8a 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tableoutput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tableoutput.adoc @@ -32,9 +32,11 @@ The Table Output transform inserts data into a relational database table. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/terafast.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/terafast.adoc index a0e8ac6ddc5..78842dfcfda 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/terafast.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/terafast.adoc @@ -35,9 +35,11 @@ The Teradata Bulkloader transform supports fastloading data into a Teradata data [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/textfileinput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/textfileinput.adoc index 184586b210e..f71f84eb249 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/textfileinput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/textfileinput.adoc @@ -34,9 +34,11 @@ The features of the transform allow you to read from a list of files or director [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/textfileoutput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/textfileoutput.adoc index f70251cdb0b..955eb9f5a9b 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/textfileoutput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/textfileoutput.adoc @@ -41,9 +41,11 @@ The `ignore manual fields` ignores any fields manually defined in the transform' [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tokenreplacement.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tokenreplacement.adoc index e80f1716128..f92e32630f9 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tokenreplacement.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tokenreplacement.adoc @@ -46,9 +46,11 @@ When replacing tokens in a file it is a best practice to output to a file also t [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/uniquerows.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/uniquerows.adoc index 10f37fe1867..197b51476de 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/uniquerows.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/uniquerows.adoc @@ -34,9 +34,11 @@ For unsorted input streams, check the xref:pipeline/transforms/uniquerowsbyhashs [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:cross.svg[Not Supported, 24] -!Flink! image:cross.svg[Not Supported, 24] -!Dataflow! image:cross.svg[Not Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/uniquerowsbyhashset.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/uniquerowsbyhashset.adoc index f993dd44976..8c363da0d25 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/uniquerowsbyhashset.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/uniquerowsbyhashset.adoc @@ -34,9 +34,11 @@ This transform differs from the xref:pipeline/transforms/uniquerows.adoc[Unique [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:cross.svg[Not Supported, 24] -!Flink! image:cross.svg[Not Supported, 24] -!Dataflow! image:cross.svg[Not Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:cross.svg[Not Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/update.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/update.adoc index e5065749007..8fa86985ad7 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/update.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/update.adoc @@ -36,9 +36,11 @@ If the row is found the row in the table is updated. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/userdefinedjavaclass.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/userdefinedjavaclass.adoc index 22dbc32121d..d7e153cd12b 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/userdefinedjavaclass.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/userdefinedjavaclass.adoc @@ -42,9 +42,11 @@ For this we use the https://janino-compiler.github.io/janino/[Janino^] project l [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/userdefinedjavaexpression.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/userdefinedjavaexpression.adoc index ee4462e23c6..bc0c3e9258f 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/userdefinedjavaexpression.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/userdefinedjavaexpression.adoc @@ -32,9 +32,11 @@ This transform allows you to enter Java expressions that are used to calculate n [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/validator.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/validator.adoc index 11bbbc2b6f7..c2aa039543f 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/validator.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/validator.adoc @@ -34,9 +34,11 @@ To get a list of all the validation errors you can define an xref:pipeline/error [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:cross.svg[Not Supported, 24] -!Flink! image:cross.svg[Not Supported, 24] -!Dataflow! image:cross.svg[Not Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/valuemapper.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/valuemapper.adoc index d136a6ab22b..dda87936499 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/valuemapper.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/valuemapper.adoc @@ -45,9 +45,11 @@ Source/Target: EN/English, FR/French, NL/Dutch, ES/Spanish, DE/German, ... [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/vcardinput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/vcardinput.adoc index 22a37a03fcb..0b54c361487 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/vcardinput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/vcardinput.adoc @@ -38,9 +38,11 @@ TIP: More information on the vCard format is available on https://en.wikipedia.o [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/vcardoutput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/vcardoutput.adoc index 27cfda38fd0..9e2a895ae38 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/vcardoutput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/vcardoutput.adoc @@ -34,9 +34,11 @@ You can output the generated vCard text as a field, write it to a file, or both. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/verticabulkloader.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/verticabulkloader.adoc index d0dcd23a0a3..0cc6829be26 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/verticabulkloader.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/verticabulkloader.adoc @@ -35,9 +35,11 @@ This is typically significantly faster than loading data through e.g. a Table Ou [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/webservices.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/webservices.adoc index 55fab8a5adb..82122bcc8c3 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/webservices.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/webservices.adoc @@ -32,9 +32,11 @@ The Web Services Lookup transform performs a Web Services lookup using the Web S [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/workflow-executor.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/workflow-executor.adoc index 3dc3d9fd2ef..33b1c7065c2 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/workflow-executor.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/workflow-executor.adoc @@ -32,9 +32,11 @@ The Workflow Executor transform execute a Hop workflow from within a pipeline. [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/workflow-logging.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/workflow-logging.adoc index 4de94984db8..b5b98296fb8 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/workflow-logging.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/workflow-logging.adoc @@ -37,9 +37,11 @@ The transform requires very little configuration, but provides a lot of informat [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/writetolog.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/writetolog.adoc index ebd79225fff..b3bdde87da8 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/writetolog.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/writetolog.adoc @@ -36,9 +36,11 @@ Typical use cases are logging specific values or custom logging messages to the [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xmlinputstream.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xmlinputstream.adoc index 8ca11bedc47..3f3a85e7364 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xmlinputstream.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xmlinputstream.adoc @@ -32,9 +32,11 @@ The XML Input Stream (StAX) transform reads data from XML files using the Stream [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xmljoin.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xmljoin.adoc index 62426c24eaa..b0c79ec44e0 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xmljoin.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xmljoin.adoc @@ -36,9 +36,11 @@ Only one row is produced after the join. This single row contains the fields of [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xmloutput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xmloutput.adoc index b0347c4fd31..921eb24b960 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xmloutput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xmloutput.adoc @@ -32,9 +32,11 @@ The XML Output transform allows you to write rows from any source to one or more [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:check_mark.svg[Supported, 24] -!Flink! image:check_mark.svg[Supported, 24] -!Dataflow! image:check_mark.svg[Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:check_mark.svg[Supported, 24] +!Beam Flink! image:check_mark.svg[Supported, 24] +!Beam Dataflow! image:check_mark.svg[Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xmloutputadvanced.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xmloutputadvanced.adoc index 34dd775af76..ca5fcc0f86d 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xmloutputadvanced.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xmloutputadvanced.adoc @@ -36,9 +36,11 @@ This transform complements the simpler `XML Output` transform. Use `XML Output` [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:cross.svg[Not Supported, 24] -!Flink! image:cross.svg[Not Supported, 24] -!Dataflow! image:cross.svg[Not Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:cross.svg[Not Supported, 24] +!Beam Flink! image:cross.svg[Not Supported, 24] +!Beam Dataflow! image:cross.svg[Not Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xsdvalidator.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xsdvalidator.adoc index f612ce8a89e..1e10b72feae 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xsdvalidator.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xsdvalidator.adoc @@ -43,9 +43,11 @@ See also the xref:workflow/actions/xsdvalidator.adoc[XSD Validator workflow acti [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xslt.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xslt.adoc index cdb6c04b082..86990385697 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xslt.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/xslt.adoc @@ -34,9 +34,11 @@ XSLT is short for link:http://en.wikipedia.org/wiki/XSLT[Extensible Stylesheet L [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/yamlinput.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/yamlinput.adoc index 9ee4d167eee..02f70bcb502 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/yamlinput.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/yamlinput.adoc @@ -32,9 +32,11 @@ The YAML Input transform reads data from YAML structures, files or incoming fiel [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/zipfile.adoc b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/zipfile.adoc index 25191015155..5f0435ce9e0 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/zipfile.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/zipfile.adoc @@ -32,9 +32,11 @@ The Zip File transform creates a standard ZIP archive using the options you spec [%noheader,cols="2,1a",frame=none, role="table-supported-engines"] !=== !Hop Engine! image:check_mark.svg[Supported, 24] -!Spark! image:question_mark.svg[Maybe Supported, 24] -!Flink! image:question_mark.svg[Maybe Supported, 24] -!Dataflow! image:question_mark.svg[Maybe Supported, 24] +!Single Threaded! image:check_mark.svg[Supported, 24] +!Native Spark! image:check_mark.svg[Supported, 24] +!Beam Spark! image:question_mark.svg[Maybe Supported, 24] +!Beam Flink! image:question_mark.svg[Maybe Supported, 24] +!Beam Dataflow! image:question_mark.svg[Maybe Supported, 24] !=== |=== diff --git a/docs/hop-user-manual/modules/ROOT/pages/vfs.adoc b/docs/hop-user-manual/modules/ROOT/pages/vfs.adoc index 91acb324925..52031222662 100644 --- a/docs/hop-user-manual/modules/ROOT/pages/vfs.adoc +++ b/docs/hop-user-manual/modules/ROOT/pages/vfs.adoc @@ -29,6 +29,13 @@ Apache Hop makes fervent usage of VFS. Beyond the standard VFS file system types, we have added a number which are present in the various technology stacks supported by Hop. Like the standard file systems each has its own unique name scheme which you can use. +[IMPORTANT] +==== +**Native Spark** Dataset I/O (*Spark File Input/Output*, Lake Table PATH) does *not* use Hop VFS. +Those transforms expect **Spark/Hadoop URIs** (for example `s3a://`, `hdfs://`, `file:///`), which are a different dialect from Hop schemes such as `s3://` or named MinIO connections. +See xref:pipeline/spark/paths-and-filesystems.adoc[Paths and file systems on native Spark]. +==== + == Apache Hop VFS File Systems The table below provides a quick overview of the VFS file systems supported by Apache Hop. diff --git a/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/databricks-job-run.adoc b/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/databricks-job-run.adoc new file mode 100644 index 00000000000..d2433b10f1b --- /dev/null +++ b/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/databricks-job-run.adoc @@ -0,0 +1,181 @@ +//// +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. +//// +:imagesdir: ../../../assets/images/ +:page-pagination: +:description: Databricks Job Run workflow action — run or submit Jobs API work and wait for completion. + += Databricks Job Run + +== Description + +The **Databricks Job Run** action triggers a job on a Databricks workspace via the Jobs REST API and can wait until the run finishes. It stores Job ID, Run ID, and status in workflow variables for downstream actions. + +Use a xref:metadata-types/databricks-connection.adoc[Databricks Connection] (workspace host + personal access token). This is **not** the JDBC SQL warehouse connection. + +== Modes + +[options="header",cols="1,3"] +|=== +|Mode |Behavior + +|Run existing job +|Calls `jobs/run-now` with the given **Job ID**. + +|One-time submit +|Calls `jobs/runs/submit` with a raw **Submit JSON** body (full Jobs API payload you provide). + +|Deploy & run +|Uploads fat jar, pipeline `.hpl`, and exported metadata JSON to a Volume or DBFS base path (optionally a **Spark project package** zip and **environment config** file), then creates or updates a JAR job (`org.apache.hop.spark.run.MainSpark`) with classic existing / `new_cluster` or serverless compute and runs it. See xref:pipeline/spark/databricks.adoc[Native Spark on Databricks]. +|=== + +== Wait modes + +[options="header",cols="1,3"] +|=== +|Wait mode |Behavior + +|Wait for completion +|Polls `jobs/runs/get` until the run is terminal (`SUCCESS`, `FAILED`, …) or **Timeout** is reached (0 = no limit). + +|Fire and forget +|Returns as soon as the run is accepted; does not poll. Pair with xref:workflow/actions/databricks-job-wait.adoc[Databricks Job Wait] later in the workflow (using `${DatabricksRunId}`). +|=== + +If the parent workflow is stopped while waiting, the action attempts to cancel the Databricks run. + +== Result variables + +Default variable names (configurable on the **Run** tab): + +|=== +|Variable |Content + +|`DatabricksJobId` +|Job ID (when known) + +|`DatabricksRunId` +|Run ID + +|`DatabricksStatus` +|Life-cycle or result state (`SUCCESS`, `FAILED`, `PENDING`, …) + +|`DatabricksRunPageUrl` +|Run page URL when returned by the API + +|`DatabricksError` +|Error message on failure +|=== + +== Options + +=== Job tab + +|=== +|Option |Description + +|Databricks connection +|xref:metadata-types/databricks-connection.adoc[Databricks Connection] metadata name + +|Mode +|Run existing job / One-time submit / Deploy & run + +|Job ID +|Numeric job id for run-now or update-existing deploy + +|Job name / Update existing job +|Name for create; if **Update existing job** is checked, **Job ID** is reset with the new settings (Deploy & run) + +|Submit JSON +|Full JSON body for runs/submit (one-time mode) +|=== + +=== Deploy tab + +|=== +|Option |Description + +|Fat jar / Pipeline / Run configuration +|Deploy mode: local fat jar (prefer `hop-conf --generate-fat-jar … --spark-client-version=native-provided`), entry pipeline path, and Native Spark run configuration name present in the metadata export (or project package) + +|DBFS / Volume base directory +|Upload root for deploy artifacts. Fat jar: content-addressed `hop-native-.jar` (+ `.sha256` sidecar) so Databricks job libraries refresh when jar bytes change (fixed-name overwrite is not enough on long-lived Dedicated clusters — see xref:pipeline/spark/databricks.adoc#lesson-fat-jar-library-uri[Databricks lessons]). Per entry pipeline stem: `pipeline-{stem}.hpl`, `metadata-{stem}.json`, `hop-spark-package-{stem}.zip`, `env-config-{stem}.json`. Prefer a Unity Catalog volume when public DBFS root is disabled, e.g. `/Volumes/catalog/schema/volume/hop`. +|=== + +=== Package tab + +|=== +|Option |Description + +|Upload Spark project package +|When checked, export (or upload an existing zip) a **Native Spark project package** so nested Simple Mapping / Pipeline Executor files under `PROJECT_HOME` resolve on the cluster. MainSpark receives `--HopProjectPackage=…`. Same layout as *Tools → Export project package for Native Spark*. Requires the native Spark engine plugin when auto-exporting. + +|Project home / Project package file +|Home used for export (default `PROJECT_HOME`). Optional existing package zip skips export. + +|Environment config file +|Optional described-variables JSON uploaded as `env-config.json` and passed to MainSpark as `--HopConfigFile` (same as MainSpark’s optional env argument). +|=== + +=== Compute tab + +|=== +|Option |Description + +|Cluster / compute +|**Classic existing:** paste the all-purpose cluster id — cluster **access mode must be Dedicated** (not Standard/shared; Standard is Spark Connect and has no `SparkContext`). **Classic job cluster:** enter `new_cluster` (ephemeral cluster for the run). **Serverless:** enter `serverless` (not supported for full Native Spark RDD workloads that need `SparkContext`). + +|new_cluster spark_version / node_type_id / num_workers / JSON +|Used when compute is `new_cluster`. Defaults: `18.2.x-scala2.13`, `i3.xlarge` (AWS), `1`. Prefer `i3.*` on AWS (local NVMe); `m5.xlarge` often needs EBS. Optional full JSON can set `spark_conf` (e.g. match `spark.executor.cores` / `spark.driver.cores` to the node’s vCPUs — see xref:pipeline/spark/databricks.adoc#spark-conf-cores[Native Spark on Databricks]). + +|Serverless environment_key / client +|Used when compute is `serverless`. Defaults: key `default`, client `4`. +|=== + +=== Run tab + +|=== +|Option |Description + +|Wait mode +|Wait for completion or fire and forget + +|Timeout (seconds) +|Maximum wait; `0` disables the timeout. For **Deploy & run** with classic `new_cluster`, allow for first jar upload **and** job cluster cold start (often **many minutes** of Pending/Initializing — see xref:pipeline/spark/databricks.adoc[Native Spark on Databricks]) + +|Poll interval (seconds) +|Delay between status polls (default 15) + +|Result variable names +|Override default variable names above +|=== + +== Related + +* xref:pipeline/spark/databricks.adoc[Native Spark on Databricks] — workspace tiers, classic vs serverless, UC Volumes, Deploy & run checklist +* xref:metadata-types/databricks-connection.adoc[Databricks Connection] +* xref:workflow/actions/databricks-job-wait.adoc[Databricks Job Wait] — poll a run after fire-and-forget +* xref:pipeline/spark/getting-started-with-native-spark.adoc[Native Spark] — fat jar / MainSpark for JAR tasks on the cluster +* xref:database/databases/databricks.adoc[Databricks JDBC] — SQL warehouse (different use case) + +=== Deploy tips + +* See the full guide: xref:pipeline/spark/databricks.adoc[Native Spark on Databricks]. +* Generate a fat jar with `hop-conf.sh --generate-fat-jar=… --spark-client-version=native-provided` when the cluster already provides Spark. +* The run configuration name must exist in the project metadata that is exported at deploy time. +* **UC Volumes** (`/Volumes/…`): uploads use the Files API (`PUT /api/2.0/fs/files/…`, up to ~5 GiB). +* **Classic DBFS** (`dbfs:/FileStore/…`): uploads use DBFS create / add-block / close. Public DBFS root is often disabled on newer workspaces — use a volume instead. +* Large jar uploads can take several minutes. diff --git a/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/databricks-job-wait.adoc b/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/databricks-job-wait.adoc new file mode 100644 index 00000000000..cb6a2f68a6c --- /dev/null +++ b/docs/hop-user-manual/modules/ROOT/pages/workflow/actions/databricks-job-wait.adoc @@ -0,0 +1,66 @@ +//// +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. +//// +:imagesdir: ../../../assets/images/ +:page-pagination: +:description: Databricks Job Wait — poll a job run until it completes. + += Databricks Job Wait + +== Description + +**Databricks Job Wait** polls the Jobs API (`jobs/runs/get`) until a run reaches a terminal state (`SUCCESS`, `FAILED`, …). Use it after xref:workflow/actions/databricks-job-run.adoc[Databricks Job Run] in **Fire and forget** mode, or when another system already started a run and you only have a **Run ID**. + +There is no dedicated “wait” endpoint on Databricks: this action repeatedly GETs run status, sleeps for the poll interval, and exits when the run finishes or the timeout is hit. + +== Typical pattern + +. Job Run with **Fire and forget** → sets `${DatabricksRunId}` +. Other workflow actions (notifications, parallel prep, …) +. **Job Wait** with Run ID `${DatabricksRunId}` → success/failure hops + +For “submit and block on this step,” use Job Run’s built-in **Wait for completion** instead. + +== Options + +[options="header",cols="1,3"] +|=== +|Option |Description + +|Databricks connection +|xref:metadata-types/databricks-connection.adoc[Databricks Connection] (PAT) + +|Run ID +|Numeric run id; default `${DatabricksRunId}` + +|Timeout (seconds) +|Maximum wait; `0` = no timeout + +|Poll interval (seconds) +|Delay between status checks (default 15) + +|Cancel run if workflow is stopped +|If checked, calls `jobs/runs/cancel` when the parent workflow stops + +|Result variables +|Same defaults as Job Run: `DatabricksJobId`, `DatabricksRunId`, `DatabricksStatus`, `DatabricksRunPageUrl`, `DatabricksError` +|=== + +== Related + +* xref:workflow/actions/databricks-job-run.adoc[Databricks Job Run] +* xref:pipeline/spark/databricks.adoc[Native Spark on Databricks] +* xref:metadata-types/databricks-connection.adoc[Databricks Connection] diff --git a/engine/src/main/java/org/apache/hop/execution/caching/BaseCachingExecutionInfoLocation.java b/engine/src/main/java/org/apache/hop/execution/caching/BaseCachingExecutionInfoLocation.java index 77fb89ebbea..40ebdbd676f 100644 --- a/engine/src/main/java/org/apache/hop/execution/caching/BaseCachingExecutionInfoLocation.java +++ b/engine/src/main/java/org/apache/hop/execution/caching/BaseCachingExecutionInfoLocation.java @@ -43,6 +43,7 @@ import org.apache.hop.core.variables.IVariables; import org.apache.hop.execution.Execution; import org.apache.hop.execution.ExecutionData; +import org.apache.hop.execution.ExecutionDataBuilder; import org.apache.hop.execution.ExecutionInfoLocation; import org.apache.hop.execution.ExecutionState; import org.apache.hop.execution.ExecutionType; @@ -214,6 +215,12 @@ public synchronized void registerExecution(Execution execution) throws HopExcept ExecutionType type = execution.getExecutionType(); if (type == ExecutionType.Pipeline || type == ExecutionType.Workflow) { addExecutionToCache(execution); + // Persist immediately so distributed engines (Spark/Beam executors) can load the parent + // CacheEntry from disk before calling registerData / register child executions. + CacheEntry entry = cache.get(execution.getId()); + if (entry != null) { + persistCacheEntry(entry); + } } else { addChildExecutionToCache(execution); } @@ -309,13 +316,19 @@ protected synchronized void addExecutionToCache(Execution execution) { cache.put(execution.getId(), entry); } - protected synchronized void addChildExecutionToCache(Execution execution) { - // Find the parent in the cache. - // We'll assume that the parent cache entry isn't removed while children are still executing. + protected synchronized void addChildExecutionToCache(Execution execution) throws HopException { + // Find the parent in the cache (or load from disk for multi-process engines). // - CacheEntry entry = cache.get(execution.getParentId()); + CacheEntry entry = findCacheEntry(execution.getParentId()); if (entry != null) { entry.addChildExecution(execution); + } else { + LogChannel.GENERAL.logError( + "Unable to register child execution '" + + execution.getId() + + "': parent execution '" + + execution.getParentId() + + "' not found in cache or on disk"); } } @@ -329,45 +342,63 @@ public synchronized void updateExecutionState(ExecutionState executionState) thr } } - protected synchronized void addStateToCache(ExecutionState executionState) { + protected synchronized void addStateToCache(ExecutionState executionState) throws HopException { CacheEntry entry = cache.get(executionState.getId()); + if (entry == null) { + // Load from disk (separate process / cache eviction) — same pattern as registerData + entry = findCacheEntry(executionState.getId()); + } if (entry == null) { // Lookup by parent (happens when a pipeline is executed by a transform) entry = findCacheEntryWithParent(executionState.getParentId()); } if (entry != null) { - // This entry should always exist + // setExecutionState flags dirty so close()/timer flush include the state entry.setExecutionState(executionState); + } else { + LogChannel.GENERAL.logError( + "Unable to update execution state for '" + + executionState.getId() + + "': parent entry not found in cache or on disk"); } } protected CacheEntry findCacheEntryWithParent(String parentId) { + if (StringUtils.isEmpty(parentId)) { + return null; + } Collection values = cache.values(); for (CacheEntry cacheEntry : values) { if (cacheEntry.getExecution() == null) { continue; } - if (cacheEntry.getExecution().getParentId().equals(parentId)) { + String execParent = cacheEntry.getExecution().getParentId(); + if (parentId.equals(execParent)) { return cacheEntry; } if (cacheEntry.getExecutionState() == null) { continue; } - if (cacheEntry.getExecutionState().getId().equals(parentId)) { + if (parentId.equals(cacheEntry.getExecutionState().getId())) { return cacheEntry; } } return null; } - protected synchronized void addChildStateToCache(ExecutionState executionState) { + protected synchronized void addChildStateToCache(ExecutionState executionState) + throws HopException { CacheEntry entry = cache.get(executionState.getParentId()); + if (entry == null) { + // Load parent from disk (Spark/Beam executors have a separate empty in-memory cache) + entry = findCacheEntry(executionState.getParentId()); + } if (entry == null) { // Lookup by parent (happens when a pipeline is executed by a transform) entry = findCacheEntryWithParent(executionState.getParentId()); } if (entry != null) { - // This parent entry should always exit + // This parent entry should always exist entry.addChildExecutionState(executionState); } } @@ -465,17 +496,32 @@ public synchronized String getExecutionStateLoggingText(String executionId, int */ @Override public synchronized void registerData(ExecutionData data) throws HopException { - // The ownerId in the data refers to the execution ID of the transform or action + // The ownerId in the data refers to the execution ID of the transform or action. + // Parent may only exist on disk (driver registered it; this process is an executor). // CacheEntry entry = findCacheEntry(data.getParentId()); if (entry != null) { entry.addExecutionData(data); + // Flush promptly so other processes can merge samples when they persist parent state + persistCacheEntry(entry); + } else { + LogChannel.GENERAL.logError( + "Unable to register execution data for owner '" + + data.getOwnerId() + + "': parent execution '" + + data.getParentId() + + "' not found in cache or on disk"); } } protected static void addChildIds(CacheEntry entry, Set ids) { for (String childId : entry.getChildIds()) { Execution childExecution = entry.getChildExecution(childId); + // getChildIds() also includes owners that only contributed sample data or state + // (e.g. local engine "all-transforms") without a full child Execution object. + if (childExecution == null) { + continue; + } // We're only interested to know about pipelines and workflows here. // if (childExecution.getExecutionType() == ExecutionType.Pipeline @@ -489,6 +535,10 @@ protected static void addChildIds( CacheEntry entry, Set ids, IExecutionSelector selector) { for (String childId : entry.getChildIds()) { Execution childExecution = entry.getChildExecution(childId); + // Data-only owners (no Execution) are not selectable as top-level children. + if (childExecution == null) { + continue; + } if (!selector.isSelected(childExecution)) { continue; } @@ -591,6 +641,18 @@ public ExecutionData getExecutionData(String parentExecutionId, String execution if (cacheEntry == null) { return null; } + + // Local engine: all transform samples under a single "all-transforms" owner + if (executionId == null) { + ExecutionData allTransforms = cacheEntry.getExecutionData("all-transforms"); + if (allTransforms != null) { + return allTransforms; + } + // Beam/Spark: per-transform (or per-copy) ExecutionData under the parent CacheEntry. + // Aggregate so the GUI can resolve samples without a separate findChildIds round-trip. + return aggregateChildExecutionData(cacheEntry, parentExecutionId); + } + ExecutionData data = cacheEntry.getExecutionData(executionId); if (data == null) { // Retry for the exception for transforms: "all-transforms" stored together. @@ -603,6 +665,40 @@ public ExecutionData getExecutionData(String parentExecutionId, String execution } } + /** + * Merge every {@link ExecutionData} stored under a parent cache entry (Beam/Spark style) into one + * builder payload the GUI can filter by transform name. + */ + private static ExecutionData aggregateChildExecutionData( + CacheEntry cacheEntry, String parentExecutionId) { + Map byOwner = cacheEntry.getChildExecutionData(); + if (byOwner == null || byOwner.isEmpty()) { + return null; + } + ExecutionDataBuilder builder = + ExecutionDataBuilder.of() + .withParentId(parentExecutionId) + .withOwnerId("all-transforms") + .withExecutionType(ExecutionType.Transform) + .withFinished(true) + .withCollectionDate(new Date()); + boolean any = false; + for (ExecutionData child : byOwner.values()) { + if (child == null) { + continue; + } + if (child.getDataSets() != null && !child.getDataSets().isEmpty()) { + builder.addDataSets(child.getDataSets()); + any = true; + } + if (child.getSetMetaData() != null && !child.getSetMetaData().isEmpty()) { + builder.addSetMeta(child.getSetMetaData()); + any = true; + } + } + return any ? builder.build() : null; + } + @Override public Execution findLastExecution(ExecutionType executionType, String name) throws HopException { try { diff --git a/engine/src/main/java/org/apache/hop/execution/caching/CacheEntry.java b/engine/src/main/java/org/apache/hop/execution/caching/CacheEntry.java index a2deca8b0d1..6fa9d75d86e 100644 --- a/engine/src/main/java/org/apache/hop/execution/caching/CacheEntry.java +++ b/engine/src/main/java/org/apache/hop/execution/caching/CacheEntry.java @@ -21,12 +21,13 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import java.io.FileOutputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import lombok.AccessLevel; import lombok.Getter; import lombok.Setter; import org.apache.commons.lang3.StringUtils; @@ -58,6 +59,9 @@ public class CacheEntry { private Execution execution; // The parent execution: pipeline or workflow + // Custom getter/setter (below) so we flag dirty when state is updated after an early persist. + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) private ExecutionState executionState; // All the child transform/action executions @@ -95,20 +99,24 @@ public CacheEntry() { */ public void writeToDisk(String rootFolder) throws HopException { String targetFilename = calculateFilename(rootFolder); - String filename = calculateFilename(rootFolder) + ".new"; - try (FileOutputStream fos = new FileOutputStream(filename)) { - // Serialize this object to JSON in a file + String filename = targetFilename + ".new"; + // Use Hop VFS (not java.io.FileOutputStream): rootFolder is often a VFS URI such as + // file:///data/hop-data/executions when resolved from ${HOP_DATA}. FileOutputStream treats + // "file://…" as a literal path and fails with FileNotFoundException even when the folder + // was created successfully via VFS. + try (OutputStream os = HopVfs.getOutputStream(filename, false)) { ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.writeValue(fos, this); + objectMapper.writeValue(os, this); } catch (Exception e) { - throw new HopException( - "Error writing cache entry to file '" + calculateFilename(rootFolder) + "'", e); + throw new HopException("Error writing cache entry to file '" + targetFilename + "'", e); } // Now delete the old file and rename the new one. // try { FileObject targetFileObject = HopVfs.getFileObject(targetFilename); - targetFileObject.delete(); + if (targetFileObject.exists()) { + targetFileObject.delete(); + } FileObject fileObject = HopVfs.getFileObject(filename); fileObject.moveTo(targetFileObject); } catch (Exception e) { @@ -140,7 +148,35 @@ public void deleteFromDisk(String rootFolder) throws HopException { * @param rootFolder the root folder to store the */ public String calculateFilename(String rootFolder) { - return rootFolder + Const.FILE_SEPARATOR + id + ".json"; + if (StringUtils.isEmpty(rootFolder)) { + return id + ".json"; + } + // Avoid double separators when the configured folder ends with / or \ + String base = rootFolder; + if (base.endsWith("/") || base.endsWith("\\")) { + return base + id + ".json"; + } + // Prefer / for VFS URIs (file://…); Const.FILE_SEPARATOR is wrong on Windows for file:// roots + if (base.contains("://") || base.startsWith("file:")) { + return base + "/" + id + ".json"; + } + return base + Const.FILE_SEPARATOR + id + ".json"; + } + + /** + * Must flag dirty: top-level {@code registerExecution} often persists immediately (dirty=false). + * A later {@code updateExecutionState} (timer or end-of-run) would otherwise sit only in memory + * and never flush on {@code close()} when no further children dirty the entry — short nested + * workflows then appear in the GUI as files without state and are filtered out. + */ + public void setExecutionState(ExecutionState executionState) { + this.executionState = executionState; + flagDirty(); + } + + public ExecutionState getExecutionState() { + flagRead(); + return executionState; } public void addChildExecution(Execution childExecution) { @@ -220,8 +256,20 @@ public boolean isTooOld(int maxAge) { return lastWritten != null && System.currentTimeMillis() - lastWritten.getTime() > maxAge; } + /** + * IDs of child transform/action executions. Includes registered children and owners that only + * contributed sample data or state (Beam/Spark may register data before a full child Execution). + */ public List getChildIds() { - return new ArrayList<>(childExecutions.keySet()); + // Preserve insertion-ish order: executions first, then states, then data-only owners + java.util.LinkedHashSet ids = new java.util.LinkedHashSet<>(childExecutions.keySet()); + if (childExecutionStates != null) { + ids.addAll(childExecutionStates.keySet()); + } + if (childExecutionData != null) { + ids.addAll(childExecutionData.keySet()); + } + return new ArrayList<>(ids); } public void calculateSummary() { diff --git a/engine/src/main/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocation.java b/engine/src/main/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocation.java index 3dbd8d2e97c..645e0245b51 100644 --- a/engine/src/main/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocation.java +++ b/engine/src/main/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocation.java @@ -23,6 +23,7 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.Date; +import java.util.Map; import java.util.Set; import lombok.Getter; import lombok.Setter; @@ -31,11 +32,13 @@ import org.apache.commons.vfs2.FileObject; import org.apache.commons.vfs2.FileSelectInfo; import org.apache.commons.vfs2.FileSystemException; +import org.apache.hop.core.Const; import org.apache.hop.core.exception.HopException; import org.apache.hop.core.exception.HopFileException; import org.apache.hop.core.gui.plugin.GuiElementType; import org.apache.hop.core.gui.plugin.GuiPlugin; import org.apache.hop.core.gui.plugin.GuiWidgetElement; +import org.apache.hop.core.logging.LogChannel; import org.apache.hop.core.variables.IVariables; import org.apache.hop.core.vfs.HopVfs; import org.apache.hop.execution.ExecutionInfoLocation; @@ -98,11 +101,30 @@ public CachingFileExecutionInfoLocation clone() { @Override public void initialize(IVariables variables, IHopMetadataProvider metadataProvider) throws HopException { - // The actual root folder - // - actualRootFolder = variables.resolve(rootFolder); + // Resolve root folder with the executor's variables (pipeline or workflow). Nested engines + // (Workflow Executor, Pipeline Executor) must inherit parent variables so ${HOP_DATA} / + // ${EXECUTIONS_INFORMATION_FOLDER} resolve the same way as the top-level run. + actualRootFolder = variables != null ? variables.resolve(rootFolder) : rootFolder; - if (createParentFolder && StringUtils.isNotEmpty(actualRootFolder)) { + if (StringUtils.isEmpty(actualRootFolder)) { + throw new HopException( + "Caching file execution information location has an empty root folder" + + " (configured value was '" + + Const.NVL(rootFolder, "") + + "'). Set a path or a variable that resolves on this executor."); + } + if (actualRootFolder.contains("${") || actualRootFolder.contains("%%")) { + throw new HopException( + "Caching file execution information location root folder still contains unresolved" + + " variables after resolution: '" + + actualRootFolder + + "' (configured '" + + Const.NVL(rootFolder, "") + + "'). Ensure variables like EXECUTIONS_INFORMATION_FOLDER / HOP_DATA are set on" + + " the pipeline or workflow that opens this location (including nested executors)."); + } + + if (createParentFolder) { try { FileObject folder = HopVfs.getFileObject(actualRootFolder); if (!folder.exists()) { @@ -114,11 +136,17 @@ public void initialize(IVariables variables, IHopMetadataProvider metadataProvid } super.initialize(variables, metadataProvider); + LogChannel.GENERAL.logBasic( + "Caching file execution info location ready: rootFolder=" + actualRootFolder); } @Override protected void persistCacheEntry(CacheEntry cacheEntry) throws HopException { try { + // Multi-writer safe: driver and executors each hold a separate CacheEntry in memory. + // Merge child maps from the on-disk file so samples/children written by other processes + // are not wiped when this process flushes parent metrics/state. + mergeChildrenFromDisk(cacheEntry); // Before writing to disk, we calculate some summaries for convenience of other tools. cacheEntry.calculateSummary(); cacheEntry.writeToDisk(actualRootFolder); @@ -130,6 +158,38 @@ protected void persistCacheEntry(CacheEntry cacheEntry) throws HopException { } } + /** + * Union child maps from the existing on-disk entry into {@code cacheEntry}. In-memory values win + * on key conflicts ({@code putIfAbsent} from disk). + */ + private void mergeChildrenFromDisk(CacheEntry cacheEntry) { + if (cacheEntry == null || StringUtils.isEmpty(cacheEntry.getId())) { + return; + } + try { + CacheEntry onDisk = loadCacheEntry(cacheEntry.getId()); + if (onDisk == null) { + return; + } + mergeMap(onDisk.getChildExecutions(), cacheEntry.getChildExecutions()); + mergeMap(onDisk.getChildExecutionStates(), cacheEntry.getChildExecutionStates()); + mergeMap(onDisk.getChildExecutionData(), cacheEntry.getChildExecutionData()); + } catch (Exception e) { + // Best-effort: still write our in-memory view if merge fails + LogChannel.GENERAL.logError( + "Unable to merge on-disk cache entry before persist (non-fatal): " + e.getMessage()); + } + } + + private static void mergeMap(Map fromDisk, Map into) { + if (fromDisk == null || fromDisk.isEmpty() || into == null) { + return; + } + for (Map.Entry e : fromDisk.entrySet()) { + into.putIfAbsent(e.getKey(), e.getValue()); + } + } + @Override public void deleteCacheEntry(CacheEntry cacheEntry) throws HopException { try { @@ -203,7 +263,7 @@ protected void retrieveIds( // To add child IDs we need to load the file. // We won't store these in the cache though. // - if (!selector.isSelectingParents()) { + if (!activeSelector.isSelectingParents()) { entry = loadCacheEntry(id); if (entry != null) { addChildIds(entry, ids); diff --git a/engine/src/main/java/org/apache/hop/pipeline/transforms/file/BaseFileInputMeta.java b/engine/src/main/java/org/apache/hop/pipeline/transforms/file/BaseFileInputMeta.java index 487f497d47e..1b1c53bb42f 100644 --- a/engine/src/main/java/org/apache/hop/pipeline/transforms/file/BaseFileInputMeta.java +++ b/engine/src/main/java/org/apache/hop/pipeline/transforms/file/BaseFileInputMeta.java @@ -95,6 +95,17 @@ public String getAcceptingTransformName() { return getFileInput() == null ? null : getFileInput().getAcceptingTransformName(); } + /** + * Sets the name of the transform that provides filenames when {@link #isAcceptingFilenames()} is + * true. Used by distributed engines (Spark mini-pipelines) to re-bind the accept source to a + * local injector. + */ + public void setAcceptingTransformName(String acceptingTransformName) { + if (getFileInput() != null) { + getFileInput().setAcceptingTransformName(acceptingTransformName); + } + } + public String getAcceptingField() { return getFileInput() == null ? null : getFileInput().getAcceptingField(); } diff --git a/engine/src/main/java/org/apache/hop/workflow/engines/local/LocalWorkflowEngine.java b/engine/src/main/java/org/apache/hop/workflow/engines/local/LocalWorkflowEngine.java index 61f32968a37..d5677ffda2f 100644 --- a/engine/src/main/java/org/apache/hop/workflow/engines/local/LocalWorkflowEngine.java +++ b/engine/src/main/java/org/apache/hop/workflow/engines/local/LocalWorkflowEngine.java @@ -37,6 +37,7 @@ import org.apache.hop.core.util.ExecutorUtil; import org.apache.hop.core.variables.IVariables; import org.apache.hop.core.variables.Variables; +import org.apache.hop.execution.Execution; import org.apache.hop.execution.ExecutionBuilder; import org.apache.hop.execution.ExecutionDataBuilder; import org.apache.hop.execution.ExecutionInfoLocation; @@ -260,20 +261,29 @@ public void afterExecution( /** This method looks up the execution information location specified in the run configuration. */ public void lookupExecutionInformationLocation() { try { + if (workflowRunConfiguration == null || metadataProvider == null) { + return; + } String locationName = resolve(workflowRunConfiguration.getExecutionInfoLocationName()); if (StringUtils.isNotEmpty(locationName)) { ExecutionInfoLocation location = metadataProvider.getSerializer(ExecutionInfoLocation.class).load(locationName); if (location != null) { - executionInfoLocation = location; + // Clone so nested workflow runs do not share timer/rootFolder state + executionInfoLocation = location.clone(); IExecutionInfoLocation iLocation = executionInfoLocation.getExecutionInfoLocation(); - // Initialize the location. - // This location is closed when nothing else needs to be done. This is when the timer is - // stopped in - // stopExecutionInfoTimer(). - // + // Initialize the location with this workflow's variable space (includes inherited parent + // pipeline variables after WorkflowExecutor.initializeFrom). This is when + // ${EXECUTIONS_INFORMATION_FOLDER} / ${HOP_DATA} must resolve. + // The location is closed when the timer is stopped in stopExecutionInfoTimer(). iLocation.initialize(this, metadataProvider); + log.logBasic( + "Using execution information location '" + + locationName + + "' (logChannelId=" + + getLogChannelId() + + ")"); } else { log.logError( "Execution information location '" @@ -295,15 +305,40 @@ public void registerWorkflowExecutionInformation() { if (executionInfoLocation != null) { // Register the execution at this location // This adds metadata, variables, parameters, ... - executionInfoLocation - .getExecutionInfoLocation() - .registerExecution(ExecutionBuilder.fromExecutor(this).build()); + Execution execution = ExecutionBuilder.fromExecutor(this).build(); + rebindSparkTransformOwnerParent(execution); + executionInfoLocation.getExecutionInfoLocation().registerExecution(execution); } } catch (Exception e) { log.logError("Error registering workflow execution information (non-fatal)", e); } } + /** + * When this workflow is nested under a Native Spark mapPartitions transform (Workflow Executor), + * the parent transform is registered under a synthetic id {@code pipelineId|name|copy}. Rebind so + * the execution perspective can drill down from that transform node. + */ + private void rebindSparkTransformOwnerParent(Execution execution) { + if (execution == null) { + return; + } + String sparkOwner = resolve("Internal.Spark.TransformOwnerId"); + if (StringUtils.isNotEmpty(sparkOwner)) { + execution.setParentId(sparkOwner); + } + } + + private void rebindSparkTransformOwnerParent(ExecutionState state) { + if (state == null) { + return; + } + String sparkOwner = resolve("Internal.Spark.TransformOwnerId"); + if (StringUtils.isNotEmpty(sparkOwner)) { + state.setParentId(sparkOwner); + } + } + public void startExecutionInfoTimer() { if (executionInfoLocation == null) { return; @@ -329,6 +364,7 @@ public void run() { ExecutionState executionState = ExecutionStateBuilder.fromExecutor(LocalWorkflowEngine.this, lastLogLineNr.get()) .build(); + rebindSparkTransformOwnerParent(executionState); iLocation.updateExecutionState(executionState); if (executionState.getLastLogLineNr() != null) { lastLogLineNr.set(executionState.getLastLogLineNr()); @@ -450,6 +486,7 @@ public void stopExecutionInfoTimer() throws HopException { // ExecutionState executionState = ExecutionStateBuilder.fromExecutor(LocalWorkflowEngine.this, -1).build(); + rebindSparkTransformOwnerParent(executionState); iLocation.updateExecutionState(executionState); } finally { // Nothing more needs to be done. We can now close the location. diff --git a/engine/src/test/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocationTest.java b/engine/src/test/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocationTest.java index 405cb5330c6..568536a7a08 100644 --- a/engine/src/test/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocationTest.java +++ b/engine/src/test/java/org/apache/hop/execution/caching/CachingFileExecutionInfoLocationTest.java @@ -18,15 +18,32 @@ package org.apache.hop.execution.caching; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; import java.util.UUID; import org.apache.commons.io.FileUtils; +import org.apache.hop.core.logging.HopLogStore; +import org.apache.hop.core.row.RowBuffer; +import org.apache.hop.core.row.RowMetaBuilder; import org.apache.hop.core.variables.Variables; +import org.apache.hop.execution.Execution; +import org.apache.hop.execution.ExecutionData; +import org.apache.hop.execution.ExecutionDataBuilder; +import org.apache.hop.execution.ExecutionDataSetMeta; +import org.apache.hop.execution.ExecutionType; +import org.apache.hop.execution.IExecutionSelector; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -34,6 +51,13 @@ class CachingFileExecutionInfoLocationTest { private Path tempDir; + @BeforeAll + static void initLogging() { + if (!HopLogStore.isInitialized()) { + HopLogStore.init(); + } + } + @BeforeEach void setUp() throws Exception { tempDir = Files.createTempDirectory("hop-caching-exec-info-test"); @@ -63,6 +87,36 @@ void testInitializeCreateFolderTrue() throws Exception { } } + /** + * Spark cluster-env style: {@code HOP_DATA=file:///…} so {@code ${HOP_DATA}/executions} is a VFS + * URI. Writes must use Hop VFS, not {@code FileOutputStream} on the URI string. + */ + @Test + void registerExecutionWithFileSchemeUriRoot() throws Exception { + Path targetDir = tempDir.resolve("vfs-uri-root"); + String parentId = "parent-" + UUID.randomUUID(); + String rootUri = targetDir.toAbsolutePath().toUri().toString(); // file:///… + + CachingFileExecutionInfoLocation location = new CachingFileExecutionInfoLocation(); + location.setRootFolder(rootUri); + location.initialize(new Variables(), null); + try { + Execution parent = new Execution(); + parent.setId(parentId); + parent.setName("spark-file-uri"); + parent.setExecutionType(ExecutionType.Pipeline); + parent.setExecutionStartDate(new Date()); + parent.setRegistrationDate(new Date()); + location.registerExecution(parent); + + assertTrue( + Files.exists(targetDir.resolve(parentId + ".json")), + "cache entry should be written under file:// root via VFS"); + } finally { + location.close(); + } + } + @Test void testInitializeCreateFolderFalse() throws Exception { Path targetDir = tempDir.resolve(UUID.randomUUID().toString()); @@ -80,4 +134,216 @@ void testInitializeCreateFolderFalse() throws Exception { location.close(); } } + + /** + * GUI path: getExecutionData(parentId, null) must aggregate per-transform sample data (Beam/Spark + * style) so PipelineExecutionViewer can show rows without a separate "all-transforms" owner. + */ + @Test + void getExecutionDataNullIdAggregatesChildSamples() throws Exception { + Path root = tempDir.resolve("gui-agg"); + String parentId = "parent-" + UUID.randomUUID(); + + CachingFileExecutionInfoLocation location = new CachingFileExecutionInfoLocation(); + location.setRootFolder(root.toAbsolutePath().toString()); + location.initialize(new Variables(), null); + + Execution parent = new Execution(); + parent.setId(parentId); + parent.setName("spark-transforms"); + parent.setExecutionType(ExecutionType.Pipeline); + parent.setExecutionStartDate(new Date()); + parent.setRegistrationDate(new Date()); + location.registerExecution(parent); + + String ownerId = parentId + "|checksum|0"; + ExecutionData data = + ExecutionDataBuilder.of() + .withParentId(parentId) + .withOwnerId(ownerId) + .withExecutionType(ExecutionType.Transform) + .withCollectionDate(new Date()) + .withFinished(true) + .build(); + // Minimal non-empty payload so aggregate sees content + data.setSetMetaData( + Map.of( + "FirstOutput/checksum.0", + new ExecutionDataSetMeta( + "FirstOutput/checksum.0", ownerId, "checksum", "0", "First output rows"))); + List rows = new ArrayList<>(); + rows.add(new Object[] {"v1"}); + data.setDataSets( + Map.of( + "FirstOutput/checksum.0", + new RowBuffer(new RowMetaBuilder().addString("c").build(), rows))); + location.registerData(data); + try { + // Same CacheEntry the GUI uses after load: aggregate without a second process + ExecutionData loaded = location.getExecutionData(parentId, null); + assertNotNull(loaded, "GUI expects aggregated data for parentId + null child"); + assertNotNull(loaded.getSetMetaData()); + assertTrue(loaded.getSetMetaData().containsKey("FirstOutput/checksum.0")); + assertTrue(location.findChildIds(ExecutionType.Pipeline, parentId).contains(ownerId)); + } finally { + location.close(); + } + } + + /** + * Simulates Spark driver + executor: separate location instances (separate in-memory caches) + * sharing the same root folder. Executor registerData must load the parent from disk and survive + * a later driver close() without wiping childExecutionData. + */ + @Test + void registerDataFromSeparateProcessSurvivesDriverClose() throws Exception { + Path root = tempDir.resolve("shared"); + String parentId = "parent-" + UUID.randomUUID(); + + CachingFileExecutionInfoLocation driver = new CachingFileExecutionInfoLocation(); + driver.setRootFolder(root.toAbsolutePath().toString()); + driver.initialize(new Variables(), null); + + Execution parent = new Execution(); + parent.setId(parentId); + parent.setName("spark-transforms"); + parent.setExecutionType(ExecutionType.Pipeline); + parent.setExecutionStartDate(new Date()); + parent.setRegistrationDate(new Date()); + driver.registerExecution(parent); + + // Parent must be on disk immediately for executors + assertTrue(Files.exists(root.resolve(parentId + ".json"))); + + // Executor-side location (fresh cache) + CachingFileExecutionInfoLocation executor = new CachingFileExecutionInfoLocation(); + executor.setRootFolder(root.toAbsolutePath().toString()); + executor.initialize(new Variables(), null); + + ExecutionData data = + ExecutionDataBuilder.of() + .withParentId(parentId) + .withOwnerId(parentId + "|CheckSum|0") + .withExecutionType(ExecutionType.Transform) + .withCollectionDate(new Date()) + .withFinished(true) + .build(); + executor.registerData(data); + executor.close(); + + // Driver final flush must not drop samples written by the executor + driver.close(); + + CachingFileExecutionInfoLocation reader = new CachingFileExecutionInfoLocation(); + reader.setRootFolder(root.toAbsolutePath().toString()); + reader.initialize(new Variables(), null); + try { + ExecutionData loaded = reader.getExecutionData(parentId, parentId + "|CheckSum|0"); + assertNotNull(loaded, "expected sample data after multi-writer merge"); + assertTrue(loaded.isFinished()); + } finally { + reader.close(); + } + } + + /** + * Short nested workflows register execution then update state at end. State must be persisted + * (dirty after setExecutionState) or the GUI filters them out as null-state entries. + */ + @Test + void updateExecutionStateIsPersistedOnClose() throws Exception { + Path root = tempDir.resolve("state-dirty"); + String id = "wf-" + UUID.randomUUID(); + + CachingFileExecutionInfoLocation location = new CachingFileExecutionInfoLocation(); + location.setRootFolder(root.toAbsolutePath().toString()); + location.initialize(new Variables(), null); + try { + Execution parent = new Execution(); + parent.setId(id); + parent.setName("create-country-folder"); + parent.setExecutionType(ExecutionType.Workflow); + parent.setExecutionStartDate(new Date()); + parent.setRegistrationDate(new Date()); + location.registerExecution(parent); + + // registerExecution already flushed; end-of-run state must still write on close + org.apache.hop.execution.ExecutionState state = new org.apache.hop.execution.ExecutionState(); + state.setId(id); + state.setName("create-country-folder"); + state.setExecutionType(ExecutionType.Workflow); + state.setStatusDescription("Finished"); + state.setFailed(false); + location.updateExecutionState(state); + } finally { + location.close(); + } + + CachingFileExecutionInfoLocation reader = new CachingFileExecutionInfoLocation(); + reader.setRootFolder(root.toAbsolutePath().toString()); + reader.initialize(new Variables(), null); + try { + org.apache.hop.execution.ExecutionState loaded = reader.getExecutionState(id); + assertNotNull(loaded, "executionState must be on disk after close()"); + assertEquals("Finished", loaded.getStatusDescription()); + } finally { + reader.close(); + } + } + + /** + * Local engine (Mapping with "local" run config) stores sample rows under owner {@code + * all-transforms} without a corresponding child {@link Execution}. Refreshing the execution + * perspective must not NPE when expanding child IDs that only have data. + */ + @Test + void findExecutionIdsWithDataOnlyChildDoesNotNpe() throws Exception { + Path root = tempDir.resolve("all-transforms-child"); + String parentId = "parent-" + UUID.randomUUID(); + + CachingFileExecutionInfoLocation writer = new CachingFileExecutionInfoLocation(); + writer.setRootFolder(root.toAbsolutePath().toString()); + writer.initialize(new Variables(), null); + try { + Execution parent = new Execution(); + parent.setId(parentId); + parent.setName("upper-name"); + parent.setExecutionType(ExecutionType.Pipeline); + parent.setExecutionStartDate(new Date()); + parent.setRegistrationDate(new Date()); + writer.registerExecution(parent); + + ExecutionData data = + ExecutionDataBuilder.of() + .withParentId(parentId) + .withOwnerId(ExecutionDataBuilder.ALL_TRANSFORMS) + .withExecutionType(ExecutionType.Transform) + .withCollectionDate(new Date()) + .withFinished(true) + .build(); + writer.registerData(data); + } finally { + writer.close(); + } + + CachingFileExecutionInfoLocation reader = new CachingFileExecutionInfoLocation(); + reader.setRootFolder(root.toAbsolutePath().toString()); + reader.initialize(new Variables(), null); + try { + // Execution perspective refresh path + List viaSelector = + assertDoesNotThrow(() -> reader.findExecutionIDs(IExecutionSelector.ALL)); + assertTrue(viaSelector.contains(parentId)); + assertFalse( + viaSelector.contains(ExecutionDataBuilder.ALL_TRANSFORMS), + "data-only owner must not appear as a selectable execution id"); + + // includeChildren=true also walks addChildIds over getChildIds() + List withChildren = assertDoesNotThrow(() -> reader.getExecutionIds(true, 0)); + assertTrue(withChildren.contains(parentId)); + assertFalse(withChildren.contains(ExecutionDataBuilder.ALL_TRANSFORMS)); + } finally { + reader.close(); + } + } } diff --git a/integration-tests/beam_directrunner/main-0005-single-thread.hwf b/integration-tests/beam_directrunner/main-0005-single-thread.hwf index db94e1a5f9a..2e6b24f2d7b 100644 --- a/integration-tests/beam_directrunner/main-0005-single-thread.hwf +++ b/integration-tests/beam_directrunner/main-0005-single-thread.hwf @@ -66,6 +66,18 @@ limitations under the License. 96 + + create /tmp/0005 + Ensure output parent folder exists for Beam SINGLE_BEAM file writers + CREATE_FOLDER + + ${java.io.tmpdir}/0005 + N + N + 368 + 96 + + Run Pipeline Unit Tests @@ -120,6 +132,13 @@ limitations under the License. delete /tmp/0005/* + create /tmp/0005 + Y + Y + N + + + create /tmp/0005 0005-generate-single-file.hpl Y Y diff --git a/integration-tests/iceberg/.gitignore b/integration-tests/iceberg/.gitignore new file mode 100644 index 00000000000..73d18af376c --- /dev/null +++ b/integration-tests/iceberg/.gitignore @@ -0,0 +1,2 @@ +output/** +!output/.gitkeep diff --git a/integration-tests/iceberg/0001-read-extract.hpl b/integration-tests/iceberg/0001-read-extract.hpl new file mode 100644 index 00000000000..d982b30fbba --- /dev/null +++ b/integration-tests/iceberg/0001-read-extract.hpl @@ -0,0 +1,106 @@ + + + + + 0001-read-extract + Y + iceberg PATH read + CSV extract + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Lake Input + CSV Extract + Y + + + + + SparkLakeTableInput + Lake Input + + Y + 1 + + none + + + iceberg + PATH + file:///tmp/hop-it-iceberg/0001-table + + + NONE + + + + + + id + Integer + -1 + 0 + + + name + String + -1 + -1 + + + + + 160 + 128 + + + + SparkFileOutput + CSV Extract + + Y + 1 + + none + + + /tmp/hop-it-iceberg/0001-extract + csv + Overwrite +
Y
+ , + " + + 1 + + + + 480 + 128 + +
+ + +
diff --git a/integration-tests/iceberg/0001-validation.hpl b/integration-tests/iceberg/0001-validation.hpl new file mode 100644 index 00000000000..3c390500b2d --- /dev/null +++ b/integration-tests/iceberg/0001-validation.hpl @@ -0,0 +1,167 @@ + + + + + 0001-validation + Y + Validate 0001 extract against golden + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Read extract CSV + Verify + Y + + + + Read extract CSV + TextFileInput2 + + Y + 1 + + none + + + N + N + + + , + " + N + +
Y
+ 1 +
N
+ 1 + N + 1 + N + 80 + 0 + Y + N + + N + N + + Unix + UTF-8 + Characters + N + + /tmp/hop-it-iceberg/0001-extract/ + .*\.csv + + Y + N + CSV + None + + + + + id + Integer + # + + + + + + -1 + -1 + 0 + both + N + + + name + String + + + + + + + -1 + -1 + -1 + both + N + + + 0 + N + N + + + N + + + + + warning + + error + + line + Y + en_US + + + + + + + + + + + 160 + 128 + +
+ + Verify + Dummy + + Y + 1 + + none + + + + + 400 + 128 + + + + +
diff --git a/integration-tests/iceberg/0001-write.hpl b/integration-tests/iceberg/0001-write.hpl new file mode 100644 index 00000000000..e7f9cc015ce --- /dev/null +++ b/integration-tests/iceberg/0001-write.hpl @@ -0,0 +1,120 @@ + + + + + 0001-write + Y + iceberg PATH write 10 rows + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Seed + Lake Output + Y + + + + + Seed + DataGrid + + Y + 1 + + none + + + + 0n0 + 1n1 + 2n2 + 3n3 + 4n4 + 5n5 + 6n6 + 7n7 + 8n8 + 9n9 + + + + + id + Integer + # + -1 + 0 + + + + N + + + name + String + + -1 + -1 + + + + N + + + + + 96 + 128 + + + + SparkLakeTableOutput + Lake Output + + Y + 1 + + none + + + iceberg + PATH + file:///tmp/hop-it-iceberg/0001-table + + + Overwrite + + + + + + 320 + 128 + + + + + diff --git a/integration-tests/iceberg/0002-read-current.hpl b/integration-tests/iceberg/0002-read-current.hpl new file mode 100644 index 00000000000..8bd818ab6e4 --- /dev/null +++ b/integration-tests/iceberg/0002-read-current.hpl @@ -0,0 +1,106 @@ + + + + + 0002-read-current + Y + Read current table version + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Lake Input + CSV Extract + Y + + + + + SparkLakeTableInput + Lake Input + + Y + 1 + + none + + + iceberg + PATH + file:///tmp/hop-it-iceberg/0002-table + + + NONE + + + + + + id + Integer + -1 + 0 + + + name + String + -1 + -1 + + + + + 160 + 128 + + + + SparkFileOutput + CSV Extract + + Y + 1 + + none + + + /tmp/hop-it-iceberg/0002-extract-current + csv + Overwrite +
Y
+ , + " + + 1 + + + + 480 + 128 + +
+ + +
diff --git a/integration-tests/iceberg/0002-validation-current.hpl b/integration-tests/iceberg/0002-validation-current.hpl new file mode 100644 index 00000000000..22bbd984ae8 --- /dev/null +++ b/integration-tests/iceberg/0002-validation-current.hpl @@ -0,0 +1,167 @@ + + + + + 0002-validation-current + Y + Validate current after overwrite + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Read extract CSV + Verify + Y + + + + Read extract CSV + TextFileInput2 + + Y + 1 + + none + + + N + N + + + , + " + N + +
Y
+ 1 +
N
+ 1 + N + 1 + N + 80 + 0 + Y + N + + N + N + + Unix + UTF-8 + Characters + N + + /tmp/hop-it-iceberg/0002-extract-current/ + .*\.csv + + Y + N + CSV + None + + + + + id + Integer + # + + + + + + -1 + -1 + 0 + both + N + + + name + String + + + + + + + -1 + -1 + -1 + both + N + + + 0 + N + N + + + N + + + + + warning + + error + + line + Y + en_US + + + + + + + + + + + 160 + 128 + +
+ + Verify + Dummy + + Y + 1 + + none + + + + + 400 + 128 + + + + +
diff --git a/integration-tests/iceberg/0002-write-v1.hpl b/integration-tests/iceberg/0002-write-v1.hpl new file mode 100644 index 00000000000..0187e0642b1 --- /dev/null +++ b/integration-tests/iceberg/0002-write-v1.hpl @@ -0,0 +1,115 @@ + + + + + 0002-write-v1 + Y + Write first version (5 rows) + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Seed + Lake Output + Y + + + + + Seed + DataGrid + + Y + 1 + + none + + + + 0n0 + 1n1 + 2n2 + 3n3 + 4n4 + + + + + id + Integer + # + -1 + 0 + + + + N + + + name + String + + -1 + -1 + + + + N + + + + + 96 + 128 + + + + SparkLakeTableOutput + Lake Output + + Y + 1 + + none + + + iceberg + PATH + file:///tmp/hop-it-iceberg/0002-table + + + Overwrite + + + + + + 320 + 128 + + + + + diff --git a/integration-tests/iceberg/0002-write-v2.hpl b/integration-tests/iceberg/0002-write-v2.hpl new file mode 100644 index 00000000000..243299c9d5c --- /dev/null +++ b/integration-tests/iceberg/0002-write-v2.hpl @@ -0,0 +1,130 @@ + + + + + 0002-write-v2 + Y + Overwrite second version (20 rows) + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Seed + Lake Output + Y + + + + + Seed + DataGrid + + Y + 1 + + none + + + + 0n0 + 1n1 + 2n2 + 3n3 + 4n4 + 5n5 + 6n6 + 7n7 + 8n8 + 9n9 + 10n10 + 11n11 + 12n12 + 13n13 + 14n14 + 15n15 + 16n16 + 17n17 + 18n18 + 19n19 + + + + + id + Integer + # + -1 + 0 + + + + N + + + name + String + + -1 + -1 + + + + N + + + + + 96 + 128 + + + + SparkLakeTableOutput + Lake Output + + Y + 1 + + none + + + iceberg + PATH + file:///tmp/hop-it-iceberg/0002-table + + + Overwrite + + + + + + 320 + 128 + + + + + diff --git a/integration-tests/iceberg/0003-merge.hpl b/integration-tests/iceberg/0003-merge.hpl new file mode 100644 index 00000000000..6e7f5b01e55 --- /dev/null +++ b/integration-tests/iceberg/0003-merge.hpl @@ -0,0 +1,113 @@ + + + + + 0003-merge + Y + MERGE upsert source into target + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Source + Lake Merge + Y + + + + + Source + DataGrid + + Y + 1 + + none + + + + 1a-updated + 3c + + + + + id + Integer + # + -1 + 0 + + + + N + + + name + String + + -1 + -1 + + + + N + + + + + 96 + 128 + + + + SparkLakeTableMerge + Lake Merge + + Y + 1 + + none + + + iceberg + PATH + file:///tmp/hop-it-iceberg/0003-table + + + t.id = s.id + UPDATE_ALL + INSERT_ALL + NONE + + + + 320 + 128 + + + + + diff --git a/integration-tests/iceberg/0003-read-extract.hpl b/integration-tests/iceberg/0003-read-extract.hpl new file mode 100644 index 00000000000..0c3093d680a --- /dev/null +++ b/integration-tests/iceberg/0003-read-extract.hpl @@ -0,0 +1,106 @@ + + + + + 0003-read-extract + Y + Read table after MERGE + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Lake Input + CSV Extract + Y + + + + + SparkLakeTableInput + Lake Input + + Y + 1 + + none + + + iceberg + PATH + file:///tmp/hop-it-iceberg/0003-table + + + NONE + + + + + + id + Integer + -1 + 0 + + + name + String + -1 + -1 + + + + + 160 + 128 + + + + SparkFileOutput + CSV Extract + + Y + 1 + + none + + + /tmp/hop-it-iceberg/0003-extract + csv + Overwrite +
Y
+ , + " + + 1 + + + + 480 + 128 + +
+ + +
diff --git a/integration-tests/iceberg/0003-seed.hpl b/integration-tests/iceberg/0003-seed.hpl new file mode 100644 index 00000000000..7ca83acc5ad --- /dev/null +++ b/integration-tests/iceberg/0003-seed.hpl @@ -0,0 +1,112 @@ + + + + + 0003-seed + Y + Seed target table for MERGE + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Seed + Lake Output + Y + + + + + Seed + DataGrid + + Y + 1 + + none + + + + 1a + 2b + + + + + id + Integer + # + -1 + 0 + + + + N + + + name + String + + -1 + -1 + + + + N + + + + + 96 + 128 + + + + SparkLakeTableOutput + Lake Output + + Y + 1 + + none + + + iceberg + PATH + file:///tmp/hop-it-iceberg/0003-table + + + Overwrite + + + + + + 320 + 128 + + + + + diff --git a/integration-tests/iceberg/0003-validation.hpl b/integration-tests/iceberg/0003-validation.hpl new file mode 100644 index 00000000000..bd2c6bb73c2 --- /dev/null +++ b/integration-tests/iceberg/0003-validation.hpl @@ -0,0 +1,167 @@ + + + + + 0003-validation + Y + Validate MERGE result + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Read extract CSV + Verify + Y + + + + Read extract CSV + TextFileInput2 + + Y + 1 + + none + + + N + N + + + , + " + N + +
Y
+ 1 +
N
+ 1 + N + 1 + N + 80 + 0 + Y + N + + N + N + + Unix + UTF-8 + Characters + N + + /tmp/hop-it-iceberg/0003-extract/ + .*\.csv + + Y + N + CSV + None + + + + + id + Integer + # + + + + + + -1 + -1 + 0 + both + N + + + name + String + + + + + + + -1 + -1 + -1 + both + N + + + 0 + N + N + + + N + + + + + warning + + error + + line + Y + en_US + + + + + + + + + + + 160 + 128 + +
+ + Verify + Dummy + + Y + 1 + + none + + + + + 400 + 128 + + + + +
diff --git a/integration-tests/iceberg/README.md b/integration-tests/iceberg/README.md new file mode 100644 index 00000000000..92f9e81bfa6 --- /dev/null +++ b/integration-tests/iceberg/README.md @@ -0,0 +1,56 @@ + + +# iceberg integration tests (iceberg) + +End-to-end Hop IT for **Spark Lake Table** transforms on the native Spark engine +using **iceberg** PATH mode. + +## Prerequisites + +- Hop with `plugins/engines/spark` and connectors under `lib/delta` / `lib/iceberg` + (default assembly since lakehouse packaging PR) +- Same environment as `integration-tests/spark-native` (Java 21) + +## Scenarios + +| Workflow | What | +|----------|------| +| `main-0001-input-output` | Data Grid → Lake Output → Lake Input → CSV → golden (10 rows) | +| `main-0002-overwrite-timetravel` | Write 5, overwrite 20, read current (and time travel where applicable) | +| `main-0003-merge` | Seed (1,a)(2,b), MERGE update/insert → golden 3 rows | + +0002 validates double Overwrite + current read. Iceberg snapshot-id time travel is covered in Java unit tests (`SparkLakeTableTimeTravelTest`). + +## Run + +```bash +export HOP_LOCATION=/path/to/hop +./integration-tests/scripts/run-tests.sh iceberg +``` + +## Table paths + +PATH mode tables under `${PROJECT_HOME}/output/` (deleted by each main workflow). +Do not commit `_delta_log` / Iceberg metadata directories. + +## Work directory + +All table and extract paths use **`/tmp/hop-it-iceberg/`** (not the project `output/` folder). +That avoids Docker volume permission issues when deleting Spark `.crc` files between runs. + +Cleanup is a Shell action (`rm -rf` + `mkdir`) that always exits 0. diff --git a/integration-tests/iceberg/datasets/0001-roundtrip-golden.csv b/integration-tests/iceberg/datasets/0001-roundtrip-golden.csv new file mode 100644 index 00000000000..56b6f4860e1 --- /dev/null +++ b/integration-tests/iceberg/datasets/0001-roundtrip-golden.csv @@ -0,0 +1,11 @@ +id,name +0,n0 +1,n1 +2,n2 +3,n3 +4,n4 +5,n5 +6,n6 +7,n7 +8,n8 +9,n9 diff --git a/integration-tests/iceberg/datasets/0002-asof-golden.csv b/integration-tests/iceberg/datasets/0002-asof-golden.csv new file mode 100644 index 00000000000..7b094294c2e --- /dev/null +++ b/integration-tests/iceberg/datasets/0002-asof-golden.csv @@ -0,0 +1,6 @@ +id,name +0,n0 +1,n1 +2,n2 +3,n3 +4,n4 diff --git a/integration-tests/iceberg/datasets/0002-current-golden.csv b/integration-tests/iceberg/datasets/0002-current-golden.csv new file mode 100644 index 00000000000..503fc657cfc --- /dev/null +++ b/integration-tests/iceberg/datasets/0002-current-golden.csv @@ -0,0 +1,21 @@ +id,name +0,n0 +1,n1 +2,n2 +3,n3 +4,n4 +5,n5 +6,n6 +7,n7 +8,n8 +9,n9 +10,n10 +11,n11 +12,n12 +13,n13 +14,n14 +15,n15 +16,n16 +17,n17 +18,n18 +19,n19 diff --git a/integration-tests/iceberg/datasets/0003-merge-golden.csv b/integration-tests/iceberg/datasets/0003-merge-golden.csv new file mode 100644 index 00000000000..81ee541b70f --- /dev/null +++ b/integration-tests/iceberg/datasets/0003-merge-golden.csv @@ -0,0 +1,4 @@ +id,name +1,a-updated +2,b +3,c diff --git a/integration-tests/iceberg/dev-env-config.json b/integration-tests/iceberg/dev-env-config.json new file mode 100644 index 00000000000..16b13b09d6a --- /dev/null +++ b/integration-tests/iceberg/dev-env-config.json @@ -0,0 +1,3 @@ +{ + "variables": [] +} diff --git a/integration-tests/iceberg/hop-config.json b/integration-tests/iceberg/hop-config.json new file mode 100644 index 00000000000..d9e1e6562e0 --- /dev/null +++ b/integration-tests/iceberg/hop-config.json @@ -0,0 +1,290 @@ +{ + "variables": [ + { + "name": "HOP_LENIENT_STRING_TO_NUMBER_CONVERSION", + "value": "N", + "description": "System wide flag to allow lenient string to number conversion for backward compatibility. If this setting is set to \"Y\", an string starting with digits will be converted successfully into a number. (example: 192.168.1.1 will be converted into 192 or 192.168 or 192168 depending on the decimal and grouping symbol). The default (N) will be to throw an error if non-numeric symbols are found in the string." + }, + { + "name": "HOP_COMPATIBILITY_DB_IGNORE_TIMEZONE", + "value": "N", + "description": "System wide flag to ignore timezone while writing date/timestamp value to the database." + }, + { + "name": "HOP_LOG_SIZE_LIMIT", + "value": "0", + "description": "The log size limit for all pipelines and workflows that don't have the \"log size limit\" property set in their respective properties." + }, + { + "name": "HOP_EMPTY_STRING_DIFFERS_FROM_NULL", + "value": "N", + "description": "NULL vs Empty String. If this setting is set to Y, an empty string and null are different. Otherwise they are not." + }, + { + "name": "HOP_MAX_LOG_SIZE_IN_LINES", + "value": "0", + "description": "The maximum number of log lines that are kept internally by Hop. Set to 0 to keep all rows (default)" + }, + { + "name": "HOP_MAX_LOG_TIMEOUT_IN_MINUTES", + "value": "1440", + "description": "The maximum age (in minutes) of a log line while being kept internally by Hop. Set to 0 to keep all rows indefinitely (default)" + }, + { + "name": "HOP_MAX_WORKFLOW_TRACKER_SIZE", + "value": "5000", + "description": "The maximum number of workflow trackers kept in memory" + }, + { + "name": "HOP_MAX_ACTIONS_LOGGED", + "value": "5000", + "description": "The maximum number of action results kept in memory for logging purposes." + }, + { + "name": "HOP_MAX_LOGGING_REGISTRY_SIZE", + "value": "10000", + "description": "The maximum number of logging registry entries kept in memory for logging purposes." + }, + { + "name": "HOP_LOG_TAB_REFRESH_DELAY", + "value": "1000", + "description": "The hop log tab refresh delay." + }, + { + "name": "HOP_LOG_TAB_REFRESH_PERIOD", + "value": "1000", + "description": "The hop log tab refresh period." + }, + { + "name": "HOP_PLUGIN_CLASSES", + "value": null, + "description": "A comma delimited list of classes to scan for plugin annotations" + }, + { + "name": "HOP_PLUGIN_PACKAGES", + "value": null, + "description": "A comma delimited list of packages to scan for plugin annotations (warning: slow!!)" + }, + { + "name": "HOP_TRANSFORM_PERFORMANCE_SNAPSHOT_LIMIT", + "value": "0", + "description": "The maximum number of transform performance snapshots to keep in memory. Set to 0 to keep all snapshots indefinitely (default)" + }, + { + "name": "HOP_ROWSET_GET_TIMEOUT", + "value": "50", + "description": "The name of the variable that optionally contains an alternative rowset get timeout (in ms). This only makes a difference for extremely short lived pipelines." + }, + { + "name": "HOP_ROWSET_PUT_TIMEOUT", + "value": "50", + "description": "The name of the variable that optionally contains an alternative rowset put timeout (in ms). This only makes a difference for extremely short lived pipelines." + }, + { + "name": "HOP_CORE_TRANSFORMS_FILE", + "value": null, + "description": "The name of the project variable that will contain the alternative location of the hop-transforms.xml file. You can use this to customize the list of available internal transforms outside of the codebase." + }, + { + "name": "HOP_CORE_WORKFLOW_ACTIONS_FILE", + "value": null, + "description": "The name of the project variable that will contain the alternative location of the hop-workflow-actions.xml file." + }, + { + "name": "HOP_SERVER_OBJECT_TIMEOUT_MINUTES", + "value": "1440", + "description": "This project variable will set a time-out after which waiting, completed or stopped pipelines and workflows will be automatically cleaned up. The default value is 1440 (one day)." + }, + { + "name": "HOP_PIPELINE_PAN_JVM_EXIT_CODE", + "value": null, + "description": "Set this variable to an integer that will be returned as the Pan JVM exit code." + }, + { + "name": "HOP_DISABLE_CONSOLE_LOGGING", + "value": "N", + "description": "Set this variable to Y to disable standard Hop logging to the console. (stdout)" + }, + { + "name": "HOP_REDIRECT_STDERR", + "value": "N", + "description": "Set this variable to Y to redirect stderr to Hop logging." + }, + { + "name": "HOP_REDIRECT_STDOUT", + "value": "N", + "description": "Set this variable to Y to redirect stdout to Hop logging." + }, + { + "name": "HOP_DEFAULT_NUMBER_FORMAT", + "value": null, + "description": "The name of the variable containing an alternative default number format" + }, + { + "name": "HOP_DEFAULT_BIGNUMBER_FORMAT", + "value": null, + "description": "The name of the variable containing an alternative default bignumber format" + }, + { + "name": "HOP_DEFAULT_INTEGER_FORMAT", + "value": null, + "description": "The name of the variable containing an alternative default integer format" + }, + { + "name": "HOP_DEFAULT_DATE_FORMAT", + "value": null, + "description": "The name of the variable containing an alternative default date format" + }, + { + "name": "HOP_DEFAULT_TIMESTAMP_FORMAT", + "value": null, + "description": "The name of the variable containing an alternative default timestamp format" + }, + { + "name": "HOP_DEFAULT_SERVLET_ENCODING", + "value": null, + "description": "Defines the default encoding for servlets, leave it empty to use Java default encoding" + }, + { + "name": "HOP_FAIL_ON_LOGGING_ERROR", + "value": "N", + "description": "Set this variable to Y when you want the workflow/pipeline fail with an error when the related logging process (e.g. to a database) fails." + }, + { + "name": "HOP_AGGREGATION_MIN_NULL_IS_VALUED", + "value": "N", + "description": "Set this variable to Y to set the minimum to NULL if NULL is within an aggregate. Otherwise by default NULL is ignored by the MIN aggregate and MIN is set to the minimum value that is not NULL. See also the variable HOP_AGGREGATION_ALL_NULLS_ARE_ZERO." + }, + { + "name": "HOP_AGGREGATION_ALL_NULLS_ARE_ZERO", + "value": "N", + "description": "Set this variable to Y to return 0 when all values within an aggregate are NULL. Otherwise by default a NULL is returned when all values are NULL." + }, + { + "name": "HOP_COMPATIBILITY_TEXT_FILE_OUTPUT_APPEND_NO_HEADER", + "value": "N", + "description": "Set this variable to Y for backward compatibility for the Text File Output transform. Setting this to Ywill add no header row at all when the append option is enabled, regardless if the file is existing or not." + }, + { + "name": "HOP_PASSWORD_ENCODER_PLUGIN", + "value": "Hop", + "description": "Specifies the password encoder plugin to use by ID (Hop is the default)." + }, + { + "name": "HOP_SYSTEM_HOSTNAME", + "value": null, + "description": "You can use this variable to speed up hostname lookup. Hostname lookup is performed by Hop so that it is capable of logging the server on which a workflow or pipeline is executed." + }, + { + "name": "HOP_SERVER_JETTY_ACCEPTORS", + "value": null, + "description": "A variable to configure jetty option: acceptors for Carte" + }, + { + "name": "HOP_SERVER_JETTY_ACCEPT_QUEUE_SIZE", + "value": null, + "description": "A variable to configure jetty option: acceptQueueSize for Carte" + }, + { + "name": "HOP_SERVER_JETTY_RES_MAX_IDLE_TIME", + "value": null, + "description": "A variable to configure jetty option: lowResourcesMaxIdleTime for Carte" + }, + { + "name": "HOP_COMPATIBILITY_MERGE_ROWS_USE_REFERENCE_STREAM_WHEN_IDENTICAL", + "value": "N", + "description": "Set this variable to Y for backward compatibility for the Merge Rows (diff) transform. Setting this to Y will use the data from the reference stream (instead of the comparison stream) in case the compared rows are identical." + }, + { + "name": "HOP_SPLIT_FIELDS_REMOVE_ENCLOSURE", + "value": "false", + "description": "Set this variable to false to preserve enclosure symbol after splitting the string in the Split fields transform. Changing it to true will remove first and last enclosure symbol from the resulting string chunks." + }, + { + "name": "HOP_ALLOW_EMPTY_FIELD_NAMES_AND_TYPES", + "value": "false", + "description": "Set this variable to TRUE to allow your pipeline to pass 'null' fields and/or empty types." + }, + { + "name": "HOP_GLOBAL_LOG_VARIABLES_CLEAR_ON_EXPORT", + "value": "false", + "description": "Set this variable to false to preserve global log variables defined in pipeline / workflow Properties -> Log panel. Changing it to true will clear it when export pipeline / workflow." + }, + { + "name": "HOP_FILE_OUTPUT_MAX_STREAM_COUNT", + "value": "1024", + "description": "This project variable is used by the Text File Output transform. It defines the max number of simultaneously open files within the transform. The transform will close/reopen files as necessary to insure the max is not exceeded" + }, + { + "name": "HOP_FILE_OUTPUT_MAX_STREAM_LIFE", + "value": "0", + "description": "This project variable is used by the Text File Output transform. It defines the max number of milliseconds between flushes of files opened by the transform." + }, + { + "name": "HOP_USE_NATIVE_FILE_DIALOG", + "value": "N", + "description": "Set this value to Y if you want to use the system file open/save dialog when browsing files" + }, + { + "name": "HOP_AUTO_CREATE_CONFIG", + "value": "Y", + "description": "Set this value to N if you don't want to automatically create a hop configuration file (hop-config.json) when it's missing" + } + ], + "LocaleDefault": "en_BE", + "guiProperties": { + "FontFixedSize": "13", + "MaxUndo": "100", + "DarkMode": "Y", + "FontNoteSize": "13", + "ShowOSLook": "Y", + "FontFixedStyle": "0", + "FontNoteName": ".AppleSystemUIFont", + "FontFixedName": "Monospaced", + "FontGraphStyle": "0", + "FontDefaultSize": "13", + "GraphColorR": "255", + "FontGraphSize": "13", + "IconSize": "32", + "BackgroundColorB": "255", + "FontNoteStyle": "0", + "FontGraphName": ".AppleSystemUIFont", + "FontDefaultName": ".AppleSystemUIFont", + "GraphColorG": "255", + "UseGlobalFileBookmarks": "Y", + "FontDefaultStyle": "0", + "GraphColorB": "255", + "BackgroundColorR": "255", + "BackgroundColorG": "255", + "WorkflowDialogStyle": "RESIZE,MAX,MIN", + "LineWidth": "1", + "ContextDialogShowCategories": "Y" + }, + "projectsConfig": { + "enabled": true, + "projectMandatory": true, + "environmentMandatory": false, + "defaultProject": "default", + "defaultEnvironment": null, + "standardParentProject": "default", + "standardProjectsFolder": null, + "projectConfigurations": [ + { + "projectName": "default", + "projectHome": "${HOP_CONFIG_FOLDER}", + "configFilename": "project-config.json" + } + ], + "lifecycleEnvironments": [ + { + "name": "dev", + "purpose": "Testing", + "projectName": "default", + "configurationFiles": [ + "${PROJECT_HOME}/dev-env-config.json" + ] + } + ], + "projectLifecycles": [] + } +} \ No newline at end of file diff --git a/integration-tests/iceberg/main-0001-input-output.hwf b/integration-tests/iceberg/main-0001-input-output.hwf new file mode 100644 index 00000000000..f7dd15e5c92 --- /dev/null +++ b/integration-tests/iceberg/main-0001-input-output.hwf @@ -0,0 +1,180 @@ + + + + main-0001-input-output + Y + iceberg Lake Table Input/Output PATH round-trip + + + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + Start + + SPECIAL + + N + 0 + 0 + 60 + 12 + 0 + 1 + 1 + N + 64 + 96 + + + + clean work dir + Remove previous table/extract data under /tmp/hop-it-iceberg (always succeeds) + SHELL + + N + N + N + + + N + Y + Basic + + N + N + N + 224 + 96 + + + + 0001-write + + PIPELINE + + ${PROJECT_HOME}/0001-write.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + + Y + + N + 400 + 96 + + + + 0001-read-extract + + PIPELINE + + ${PROJECT_HOME}/0001-read-extract.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + + Y + + N + 576 + 96 + + + + Run unit tests + + RunPipelineTests + + + + 0001-validation UNIT + + + N + 752 + 96 + + + + + + Start + clean work dir + Y + Y + Y + + + clean work dir + 0001-write + Y + Y + N + + + 0001-write + 0001-read-extract + Y + Y + N + + + 0001-read-extract + Run unit tests + Y + Y + N + + + + + diff --git a/integration-tests/iceberg/main-0002-overwrite-timetravel.hwf b/integration-tests/iceberg/main-0002-overwrite-timetravel.hwf new file mode 100644 index 00000000000..dcac7cc9ffe --- /dev/null +++ b/integration-tests/iceberg/main-0002-overwrite-timetravel.hwf @@ -0,0 +1,215 @@ + + + + main-0002-overwrite-timetravel + Y + iceberg double overwrite; current read (snapshot TT covered in unit tests) + + + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + Start + + SPECIAL + + N + 0 + 0 + 60 + 12 + 0 + 1 + 1 + N + 64 + 96 + + + + clean work dir + Remove previous table/extract data under /tmp/hop-it-iceberg (always succeeds) + SHELL + + N + N + N + + + N + Y + Basic + + N + N + N + 224 + 96 + + + + write v1 + + PIPELINE + + ${PROJECT_HOME}/0002-write-v1.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + + Y + + N + 360 + 96 + + + + write v2 + + PIPELINE + + ${PROJECT_HOME}/0002-write-v2.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + + Y + + N + 520 + 96 + + + + read current + + PIPELINE + + ${PROJECT_HOME}/0002-read-current.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + + Y + + N + 680 + 96 + + + + Run unit tests + + RunPipelineTests + + + + 0002-validation-current UNIT + + + N + 860 + 96 + + + + + + Start + clean work dir + Y + Y + Y + + + clean work dir + write v1 + Y + Y + N + + + write v1 + write v2 + Y + Y + N + + + write v2 + read current + Y + Y + N + + + read current + Run unit tests + Y + Y + N + + + + + diff --git a/integration-tests/iceberg/main-0003-merge.hwf b/integration-tests/iceberg/main-0003-merge.hwf new file mode 100644 index 00000000000..aa4c16d4bde --- /dev/null +++ b/integration-tests/iceberg/main-0003-merge.hwf @@ -0,0 +1,215 @@ + + + + main-0003-merge + Y + iceberg Lake Table MERGE upsert + + + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + Start + + SPECIAL + + N + 0 + 0 + 60 + 12 + 0 + 1 + 1 + N + 64 + 96 + + + + clean work dir + Remove previous table/extract data under /tmp/hop-it-iceberg (always succeeds) + SHELL + + N + N + N + + + N + Y + Basic + + N + N + N + 224 + 96 + + + + seed + + PIPELINE + + ${PROJECT_HOME}/0003-seed.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + + Y + + N + 400 + 96 + + + + merge + + PIPELINE + + ${PROJECT_HOME}/0003-merge.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + + Y + + N + 560 + 96 + + + + read + + PIPELINE + + ${PROJECT_HOME}/0003-read-extract.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + + Y + + N + 720 + 96 + + + + Run unit tests + + RunPipelineTests + + + + 0003-validation UNIT + + + N + 880 + 96 + + + + + + Start + clean work dir + Y + Y + Y + + + clean work dir + seed + Y + Y + N + + + seed + merge + Y + Y + N + + + merge + read + Y + Y + N + + + read + Run unit tests + Y + Y + N + + + + + diff --git a/integration-tests/iceberg/metadata/dataset/0001-roundtrip-golden.json b/integration-tests/iceberg/metadata/dataset/0001-roundtrip-golden.json new file mode 100644 index 00000000000..a1e025d75b6 --- /dev/null +++ b/integration-tests/iceberg/metadata/dataset/0001-roundtrip-golden.json @@ -0,0 +1,24 @@ +{ + "base_filename": "0001-roundtrip-golden.csv", + "name": "0001-roundtrip-golden", + "description": "10-row PATH round-trip", + "dataset_fields": [ + { + "field_comment": "", + "field_length": -1, + "field_type": 5, + "field_precision": 0, + "field_name": "id", + "field_format": "#" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "name", + "field_format": "" + } + ], + "folder_name": "" +} diff --git a/integration-tests/iceberg/metadata/dataset/0002-asof-golden.json b/integration-tests/iceberg/metadata/dataset/0002-asof-golden.json new file mode 100644 index 00000000000..dae2f844858 --- /dev/null +++ b/integration-tests/iceberg/metadata/dataset/0002-asof-golden.json @@ -0,0 +1,24 @@ +{ + "base_filename": "0002-asof-golden.csv", + "name": "0002-asof-golden", + "description": "Time travel first write (5 rows)", + "dataset_fields": [ + { + "field_comment": "", + "field_length": -1, + "field_type": 5, + "field_precision": 0, + "field_name": "id", + "field_format": "#" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "name", + "field_format": "" + } + ], + "folder_name": "" +} diff --git a/integration-tests/iceberg/metadata/dataset/0002-current-golden.json b/integration-tests/iceberg/metadata/dataset/0002-current-golden.json new file mode 100644 index 00000000000..432e22ed988 --- /dev/null +++ b/integration-tests/iceberg/metadata/dataset/0002-current-golden.json @@ -0,0 +1,24 @@ +{ + "base_filename": "0002-current-golden.csv", + "name": "0002-current-golden", + "description": "After second overwrite (current)", + "dataset_fields": [ + { + "field_comment": "", + "field_length": -1, + "field_type": 5, + "field_precision": 0, + "field_name": "id", + "field_format": "#" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "name", + "field_format": "" + } + ], + "folder_name": "" +} diff --git a/integration-tests/iceberg/metadata/dataset/0003-merge-golden.json b/integration-tests/iceberg/metadata/dataset/0003-merge-golden.json new file mode 100644 index 00000000000..561a4c69a00 --- /dev/null +++ b/integration-tests/iceberg/metadata/dataset/0003-merge-golden.json @@ -0,0 +1,24 @@ +{ + "base_filename": "0003-merge-golden.csv", + "name": "0003-merge-golden", + "description": "After MERGE upsert", + "dataset_fields": [ + { + "field_comment": "", + "field_length": -1, + "field_type": 5, + "field_precision": 0, + "field_name": "id", + "field_format": "#" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "name", + "field_format": "" + } + ], + "folder_name": "" +} diff --git a/integration-tests/iceberg/metadata/pipeline-run-configuration/local.json b/integration-tests/iceberg/metadata/pipeline-run-configuration/local.json new file mode 100644 index 00000000000..76f9ec72c22 --- /dev/null +++ b/integration-tests/iceberg/metadata/pipeline-run-configuration/local.json @@ -0,0 +1,17 @@ +{ + "engineRunConfiguration": { + "Local": { + "feedback_size": "50000", + "sample_size": "100", + "sample_type_in_gui": "Last", + "rowset_size": "10000", + "safe_mode": false, + "show_feedback": false, + "topo_sort": false, + "gather_metrics": false + } + }, + "configurationVariables": [], + "name": "local", + "description": "Local Hop pipeline engine for validation" +} diff --git a/integration-tests/iceberg/metadata/pipeline-run-configuration/spark-local.json b/integration-tests/iceberg/metadata/pipeline-run-configuration/spark-local.json new file mode 100644 index 00000000000..a9f352e5f7b --- /dev/null +++ b/integration-tests/iceberg/metadata/pipeline-run-configuration/spark-local.json @@ -0,0 +1,18 @@ +{ + "engineRunConfiguration": { + "SparkPipelineEngine": { + "sparkMaster": "local[2]", + "sparkAppName": "hop-it-iceberg", + "sparkConfigs": "spark.ui.enabled=false\nspark.driver.host=localhost", + "fatJar": "", + "tempLocation": "/tmp", + "driverMemory": "", + "executorMemory": "", + "executorCores": "", + "pluginsToStage": "" + } + }, + "configurationVariables": [], + "name": "spark-local", + "description": "Native Spark local[2] for iceberg lake table ITs" +} diff --git a/integration-tests/iceberg/metadata/unit-test/0001-validation UNIT.json b/integration-tests/iceberg/metadata/unit-test/0001-validation UNIT.json new file mode 100644 index 00000000000..3c4c89d8a6d --- /dev/null +++ b/integration-tests/iceberg/metadata/unit-test/0001-validation UNIT.json @@ -0,0 +1,32 @@ +{ + "variableValues": [], + "database_replacements": [], + "autoOpening": true, + "basePath": "", + "golden_data_sets": [ + { + "field_mappings": [ + { + "transform_field": "id", + "data_set_field": "id" + }, + { + "transform_field": "name", + "data_set_field": "name" + } + ], + "field_order": [ + "id" + ], + "data_set_name": "0001-roundtrip-golden", + "transform_name": "Verify" + } + ], + "input_data_sets": [], + "name": "0001-validation UNIT", + "description": "Golden compare for PATH round-trip", + "persist_filename": "", + "trans_test_tweaks": [], + "pipeline_filename": "./0001-validation.hpl", + "test_type": "UNIT_TEST" +} diff --git a/integration-tests/iceberg/metadata/unit-test/0002-validation-current UNIT.json b/integration-tests/iceberg/metadata/unit-test/0002-validation-current UNIT.json new file mode 100644 index 00000000000..1018f4be8bc --- /dev/null +++ b/integration-tests/iceberg/metadata/unit-test/0002-validation-current UNIT.json @@ -0,0 +1,32 @@ +{ + "variableValues": [], + "database_replacements": [], + "autoOpening": true, + "basePath": "", + "golden_data_sets": [ + { + "field_mappings": [ + { + "transform_field": "id", + "data_set_field": "id" + }, + { + "transform_field": "name", + "data_set_field": "name" + } + ], + "field_order": [ + "id" + ], + "data_set_name": "0002-current-golden", + "transform_name": "Verify" + } + ], + "input_data_sets": [], + "name": "0002-validation-current UNIT", + "description": "Current table after second overwrite", + "persist_filename": "", + "trans_test_tweaks": [], + "pipeline_filename": "./0002-validation-current.hpl", + "test_type": "UNIT_TEST" +} diff --git a/integration-tests/iceberg/metadata/unit-test/0003-validation UNIT.json b/integration-tests/iceberg/metadata/unit-test/0003-validation UNIT.json new file mode 100644 index 00000000000..8c61cf93665 --- /dev/null +++ b/integration-tests/iceberg/metadata/unit-test/0003-validation UNIT.json @@ -0,0 +1,32 @@ +{ + "variableValues": [], + "database_replacements": [], + "autoOpening": true, + "basePath": "", + "golden_data_sets": [ + { + "field_mappings": [ + { + "transform_field": "id", + "data_set_field": "id" + }, + { + "transform_field": "name", + "data_set_field": "name" + } + ], + "field_order": [ + "id" + ], + "data_set_name": "0003-merge-golden", + "transform_name": "Verify" + } + ], + "input_data_sets": [], + "name": "0003-validation UNIT", + "description": "MERGE upsert golden", + "persist_filename": "", + "trans_test_tweaks": [], + "pipeline_filename": "./0003-validation.hpl", + "test_type": "UNIT_TEST" +} diff --git a/integration-tests/iceberg/metadata/workflow-run-configuration/local.json b/integration-tests/iceberg/metadata/workflow-run-configuration/local.json new file mode 100644 index 00000000000..911da1e1fe6 --- /dev/null +++ b/integration-tests/iceberg/metadata/workflow-run-configuration/local.json @@ -0,0 +1,9 @@ +{ + "engineRunConfiguration": { + "Local": { + "safe_mode": false + } + }, + "name": "local", + "description": "Local workflow engine" +} diff --git a/integration-tests/iceberg/output/.gitkeep b/integration-tests/iceberg/output/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/integration-tests/iceberg/project-config.json b/integration-tests/iceberg/project-config.json new file mode 100644 index 00000000000..3b7af5d8b73 --- /dev/null +++ b/integration-tests/iceberg/project-config.json @@ -0,0 +1,15 @@ +{ + "metadataBaseFolder": "${PROJECT_HOME}/metadata", + "unitTestsBasePath": "${PROJECT_HOME}", + "dataSetsCsvFolder": "${PROJECT_HOME}/datasets", + "enforcingExecutionInHome": true, + "config": { + "variables": [ + { + "name": "HOP_LICENSE_HEADER_FILE", + "value": "${PROJECT_HOME}/../asf-header.txt", + "description": "ASF license header for serialized pipelines/workflows" + } + ] + } +} diff --git a/integration-tests/lakehouse/.gitignore b/integration-tests/lakehouse/.gitignore new file mode 100644 index 00000000000..73d18af376c --- /dev/null +++ b/integration-tests/lakehouse/.gitignore @@ -0,0 +1,2 @@ +output/** +!output/.gitkeep diff --git a/integration-tests/lakehouse/0001-read-extract.hpl b/integration-tests/lakehouse/0001-read-extract.hpl new file mode 100644 index 00000000000..d22696ad05c --- /dev/null +++ b/integration-tests/lakehouse/0001-read-extract.hpl @@ -0,0 +1,106 @@ + + + + + 0001-read-extract + Y + delta PATH read + CSV extract + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Lake Input + CSV Extract + Y + + + + + SparkLakeTableInput + Lake Input + + Y + 1 + + none + + + delta + PATH + /tmp/hop-it-lakehouse/0001-table + + + NONE + + + + + + id + Integer + -1 + 0 + + + name + String + -1 + -1 + + + + + 160 + 128 + + + + SparkFileOutput + CSV Extract + + Y + 1 + + none + + + /tmp/hop-it-lakehouse/0001-extract + csv + Overwrite +
Y
+ , + " + + 1 + + + + 480 + 128 + +
+ + +
diff --git a/integration-tests/lakehouse/0001-validation.hpl b/integration-tests/lakehouse/0001-validation.hpl new file mode 100644 index 00000000000..a21356dc6ba --- /dev/null +++ b/integration-tests/lakehouse/0001-validation.hpl @@ -0,0 +1,167 @@ + + + + + 0001-validation + Y + Validate 0001 extract against golden + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Read extract CSV + Verify + Y + + + + Read extract CSV + TextFileInput2 + + Y + 1 + + none + + + N + N + + + , + " + N + +
Y
+ 1 +
N
+ 1 + N + 1 + N + 80 + 0 + Y + N + + N + N + + Unix + UTF-8 + Characters + N + + /tmp/hop-it-lakehouse/0001-extract/ + .*\.csv + + Y + N + CSV + None + + + + + id + Integer + # + + + + + + -1 + -1 + 0 + both + N + + + name + String + + + + + + + -1 + -1 + -1 + both + N + + + 0 + N + N + + + N + + + + + warning + + error + + line + Y + en_US + + + + + + + + + + + 160 + 128 + +
+ + Verify + Dummy + + Y + 1 + + none + + + + + 400 + 128 + + + + +
diff --git a/integration-tests/lakehouse/0001-write.hpl b/integration-tests/lakehouse/0001-write.hpl new file mode 100644 index 00000000000..5dd0cdc0d6b --- /dev/null +++ b/integration-tests/lakehouse/0001-write.hpl @@ -0,0 +1,120 @@ + + + + + 0001-write + Y + delta PATH write 10 rows + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Seed + Lake Output + Y + + + + + Seed + DataGrid + + Y + 1 + + none + + + + 0n0 + 1n1 + 2n2 + 3n3 + 4n4 + 5n5 + 6n6 + 7n7 + 8n8 + 9n9 + + + + + id + Integer + # + -1 + 0 + + + + N + + + name + String + + -1 + -1 + + + + N + + + + + 96 + 128 + + + + SparkLakeTableOutput + Lake Output + + Y + 1 + + none + + + delta + PATH + /tmp/hop-it-lakehouse/0001-table + + + Overwrite + + + + + + 320 + 128 + + + + + diff --git a/integration-tests/lakehouse/0002-read-asof.hpl b/integration-tests/lakehouse/0002-read-asof.hpl new file mode 100644 index 00000000000..3b328125bcc --- /dev/null +++ b/integration-tests/lakehouse/0002-read-asof.hpl @@ -0,0 +1,106 @@ + + + + + 0002-read-asof + Y + Time travel VERSION 0 (first write) + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Lake Input + CSV Extract + Y + + + + + SparkLakeTableInput + Lake Input + + Y + 1 + + none + + + delta + PATH + /tmp/hop-it-lakehouse/0002-table + + + VERSION + 0 + + + + + id + Integer + -1 + 0 + + + name + String + -1 + -1 + + + + + 160 + 128 + + + + SparkFileOutput + CSV Extract + + Y + 1 + + none + + + /tmp/hop-it-lakehouse/0002-extract-asof + csv + Overwrite +
Y
+ , + " + + 1 + + + + 480 + 128 + +
+ + +
diff --git a/integration-tests/lakehouse/0002-read-current.hpl b/integration-tests/lakehouse/0002-read-current.hpl new file mode 100644 index 00000000000..bbaa4911164 --- /dev/null +++ b/integration-tests/lakehouse/0002-read-current.hpl @@ -0,0 +1,106 @@ + + + + + 0002-read-current + Y + Read current table version + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Lake Input + CSV Extract + Y + + + + + SparkLakeTableInput + Lake Input + + Y + 1 + + none + + + delta + PATH + /tmp/hop-it-lakehouse/0002-table + + + NONE + + + + + + id + Integer + -1 + 0 + + + name + String + -1 + -1 + + + + + 160 + 128 + + + + SparkFileOutput + CSV Extract + + Y + 1 + + none + + + /tmp/hop-it-lakehouse/0002-extract-current + csv + Overwrite +
Y
+ , + " + + 1 + + + + 480 + 128 + +
+ + +
diff --git a/integration-tests/lakehouse/0002-validation-asof.hpl b/integration-tests/lakehouse/0002-validation-asof.hpl new file mode 100644 index 00000000000..772588ac61b --- /dev/null +++ b/integration-tests/lakehouse/0002-validation-asof.hpl @@ -0,0 +1,167 @@ + + + + + 0002-validation-asof + Y + Validate VERSION 0 time travel + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Read extract CSV + Verify + Y + + + + Read extract CSV + TextFileInput2 + + Y + 1 + + none + + + N + N + + + , + " + N + +
Y
+ 1 +
N
+ 1 + N + 1 + N + 80 + 0 + Y + N + + N + N + + Unix + UTF-8 + Characters + N + + /tmp/hop-it-lakehouse/0002-extract-asof/ + .*\.csv + + Y + N + CSV + None + + + + + id + Integer + # + + + + + + -1 + -1 + 0 + both + N + + + name + String + + + + + + + -1 + -1 + -1 + both + N + + + 0 + N + N + + + N + + + + + warning + + error + + line + Y + en_US + + + + + + + + + + + 160 + 128 + +
+ + Verify + Dummy + + Y + 1 + + none + + + + + 400 + 128 + + + + +
diff --git a/integration-tests/lakehouse/0002-validation-current.hpl b/integration-tests/lakehouse/0002-validation-current.hpl new file mode 100644 index 00000000000..ce131389f9a --- /dev/null +++ b/integration-tests/lakehouse/0002-validation-current.hpl @@ -0,0 +1,167 @@ + + + + + 0002-validation-current + Y + Validate current after overwrite + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Read extract CSV + Verify + Y + + + + Read extract CSV + TextFileInput2 + + Y + 1 + + none + + + N + N + + + , + " + N + +
Y
+ 1 +
N
+ 1 + N + 1 + N + 80 + 0 + Y + N + + N + N + + Unix + UTF-8 + Characters + N + + /tmp/hop-it-lakehouse/0002-extract-current/ + .*\.csv + + Y + N + CSV + None + + + + + id + Integer + # + + + + + + -1 + -1 + 0 + both + N + + + name + String + + + + + + + -1 + -1 + -1 + both + N + + + 0 + N + N + + + N + + + + + warning + + error + + line + Y + en_US + + + + + + + + + + + 160 + 128 + +
+ + Verify + Dummy + + Y + 1 + + none + + + + + 400 + 128 + + + + +
diff --git a/integration-tests/lakehouse/0002-write-v1.hpl b/integration-tests/lakehouse/0002-write-v1.hpl new file mode 100644 index 00000000000..816348e5cfe --- /dev/null +++ b/integration-tests/lakehouse/0002-write-v1.hpl @@ -0,0 +1,115 @@ + + + + + 0002-write-v1 + Y + Write first version (5 rows) + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Seed + Lake Output + Y + + + + + Seed + DataGrid + + Y + 1 + + none + + + + 0n0 + 1n1 + 2n2 + 3n3 + 4n4 + + + + + id + Integer + # + -1 + 0 + + + + N + + + name + String + + -1 + -1 + + + + N + + + + + 96 + 128 + + + + SparkLakeTableOutput + Lake Output + + Y + 1 + + none + + + delta + PATH + /tmp/hop-it-lakehouse/0002-table + + + Overwrite + + + + + + 320 + 128 + + + + + diff --git a/integration-tests/lakehouse/0002-write-v2.hpl b/integration-tests/lakehouse/0002-write-v2.hpl new file mode 100644 index 00000000000..716e654c75c --- /dev/null +++ b/integration-tests/lakehouse/0002-write-v2.hpl @@ -0,0 +1,130 @@ + + + + + 0002-write-v2 + Y + Overwrite second version (20 rows) + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Seed + Lake Output + Y + + + + + Seed + DataGrid + + Y + 1 + + none + + + + 0n0 + 1n1 + 2n2 + 3n3 + 4n4 + 5n5 + 6n6 + 7n7 + 8n8 + 9n9 + 10n10 + 11n11 + 12n12 + 13n13 + 14n14 + 15n15 + 16n16 + 17n17 + 18n18 + 19n19 + + + + + id + Integer + # + -1 + 0 + + + + N + + + name + String + + -1 + -1 + + + + N + + + + + 96 + 128 + + + + SparkLakeTableOutput + Lake Output + + Y + 1 + + none + + + delta + PATH + /tmp/hop-it-lakehouse/0002-table + + + Overwrite + + + + + + 320 + 128 + + + + + diff --git a/integration-tests/lakehouse/0003-merge.hpl b/integration-tests/lakehouse/0003-merge.hpl new file mode 100644 index 00000000000..0c3cc718ce4 --- /dev/null +++ b/integration-tests/lakehouse/0003-merge.hpl @@ -0,0 +1,113 @@ + + + + + 0003-merge + Y + MERGE upsert source into target + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Source + Lake Merge + Y + + + + + Source + DataGrid + + Y + 1 + + none + + + + 1a-updated + 3c + + + + + id + Integer + # + -1 + 0 + + + + N + + + name + String + + -1 + -1 + + + + N + + + + + 96 + 128 + + + + SparkLakeTableMerge + Lake Merge + + Y + 1 + + none + + + delta + PATH + /tmp/hop-it-lakehouse/0003-table + + + t.id = s.id + UPDATE_ALL + INSERT_ALL + NONE + + + + 320 + 128 + + + + + diff --git a/integration-tests/lakehouse/0003-read-extract.hpl b/integration-tests/lakehouse/0003-read-extract.hpl new file mode 100644 index 00000000000..66b0d435a8f --- /dev/null +++ b/integration-tests/lakehouse/0003-read-extract.hpl @@ -0,0 +1,106 @@ + + + + + 0003-read-extract + Y + Read table after MERGE + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Lake Input + CSV Extract + Y + + + + + SparkLakeTableInput + Lake Input + + Y + 1 + + none + + + delta + PATH + /tmp/hop-it-lakehouse/0003-table + + + NONE + + + + + + id + Integer + -1 + 0 + + + name + String + -1 + -1 + + + + + 160 + 128 + + + + SparkFileOutput + CSV Extract + + Y + 1 + + none + + + /tmp/hop-it-lakehouse/0003-extract + csv + Overwrite +
Y
+ , + " + + 1 + + + + 480 + 128 + +
+ + +
diff --git a/integration-tests/lakehouse/0003-seed.hpl b/integration-tests/lakehouse/0003-seed.hpl new file mode 100644 index 00000000000..3c62db11f34 --- /dev/null +++ b/integration-tests/lakehouse/0003-seed.hpl @@ -0,0 +1,112 @@ + + + + + 0003-seed + Y + Seed target table for MERGE + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Seed + Lake Output + Y + + + + + Seed + DataGrid + + Y + 1 + + none + + + + 1a + 2b + + + + + id + Integer + # + -1 + 0 + + + + N + + + name + String + + -1 + -1 + + + + N + + + + + 96 + 128 + + + + SparkLakeTableOutput + Lake Output + + Y + 1 + + none + + + delta + PATH + /tmp/hop-it-lakehouse/0003-table + + + Overwrite + + + + + + 320 + 128 + + + + + diff --git a/integration-tests/lakehouse/0003-validation.hpl b/integration-tests/lakehouse/0003-validation.hpl new file mode 100644 index 00000000000..609bc57c6d0 --- /dev/null +++ b/integration-tests/lakehouse/0003-validation.hpl @@ -0,0 +1,167 @@ + + + + + 0003-validation + Y + Validate MERGE result + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + Read extract CSV + Verify + Y + + + + Read extract CSV + TextFileInput2 + + Y + 1 + + none + + + N + N + + + , + " + N + +
Y
+ 1 +
N
+ 1 + N + 1 + N + 80 + 0 + Y + N + + N + N + + Unix + UTF-8 + Characters + N + + /tmp/hop-it-lakehouse/0003-extract/ + .*\.csv + + Y + N + CSV + None + + + + + id + Integer + # + + + + + + -1 + -1 + 0 + both + N + + + name + String + + + + + + + -1 + -1 + -1 + both + N + + + 0 + N + N + + + N + + + + + warning + + error + + line + Y + en_US + + + + + + + + + + + 160 + 128 + +
+ + Verify + Dummy + + Y + 1 + + none + + + + + 400 + 128 + + + + +
diff --git a/integration-tests/lakehouse/README.md b/integration-tests/lakehouse/README.md new file mode 100644 index 00000000000..2bef1ee82be --- /dev/null +++ b/integration-tests/lakehouse/README.md @@ -0,0 +1,56 @@ + + +# lakehouse integration tests (delta) + +End-to-end Hop IT for **Spark Lake Table** transforms on the native Spark engine +using **delta** PATH mode. + +## Prerequisites + +- Hop with `plugins/engines/spark` and connectors under `lib/delta` / `lib/iceberg` + (default assembly since lakehouse packaging PR) +- Same environment as `integration-tests/spark-native` (Java 21) + +## Scenarios + +| Workflow | What | +|----------|------| +| `main-0001-input-output` | Data Grid → Lake Output → Lake Input → CSV → golden (10 rows) | +| `main-0002-overwrite-timetravel` | Write 5, overwrite 20, read current (and time travel where applicable) | +| `main-0003-merge` | Seed (1,a)(2,b), MERGE update/insert → golden 3 rows | + +0002 uses Delta **VERSION 0** for time travel after two Overwrites. + +## Run + +```bash +export HOP_LOCATION=/path/to/hop +./integration-tests/scripts/run-tests.sh lakehouse +``` + +## Table paths + +PATH mode tables under `${PROJECT_HOME}/output/` (deleted by each main workflow). +Do not commit `_delta_log` / Iceberg metadata directories. + +## Work directory + +All table and extract paths use **`/tmp/hop-it-lakehouse/`** (not the project `output/` folder). +That avoids Docker volume permission issues when deleting Spark `.crc` files between runs. + +Cleanup is a Shell action (`rm -rf` + `mkdir`) that always exits 0. diff --git a/integration-tests/lakehouse/datasets/0001-roundtrip-golden.csv b/integration-tests/lakehouse/datasets/0001-roundtrip-golden.csv new file mode 100644 index 00000000000..56b6f4860e1 --- /dev/null +++ b/integration-tests/lakehouse/datasets/0001-roundtrip-golden.csv @@ -0,0 +1,11 @@ +id,name +0,n0 +1,n1 +2,n2 +3,n3 +4,n4 +5,n5 +6,n6 +7,n7 +8,n8 +9,n9 diff --git a/integration-tests/lakehouse/datasets/0002-asof-golden.csv b/integration-tests/lakehouse/datasets/0002-asof-golden.csv new file mode 100644 index 00000000000..7b094294c2e --- /dev/null +++ b/integration-tests/lakehouse/datasets/0002-asof-golden.csv @@ -0,0 +1,6 @@ +id,name +0,n0 +1,n1 +2,n2 +3,n3 +4,n4 diff --git a/integration-tests/lakehouse/datasets/0002-current-golden.csv b/integration-tests/lakehouse/datasets/0002-current-golden.csv new file mode 100644 index 00000000000..503fc657cfc --- /dev/null +++ b/integration-tests/lakehouse/datasets/0002-current-golden.csv @@ -0,0 +1,21 @@ +id,name +0,n0 +1,n1 +2,n2 +3,n3 +4,n4 +5,n5 +6,n6 +7,n7 +8,n8 +9,n9 +10,n10 +11,n11 +12,n12 +13,n13 +14,n14 +15,n15 +16,n16 +17,n17 +18,n18 +19,n19 diff --git a/integration-tests/lakehouse/datasets/0003-merge-golden.csv b/integration-tests/lakehouse/datasets/0003-merge-golden.csv new file mode 100644 index 00000000000..81ee541b70f --- /dev/null +++ b/integration-tests/lakehouse/datasets/0003-merge-golden.csv @@ -0,0 +1,4 @@ +id,name +1,a-updated +2,b +3,c diff --git a/integration-tests/lakehouse/dev-env-config.json b/integration-tests/lakehouse/dev-env-config.json new file mode 100644 index 00000000000..16b13b09d6a --- /dev/null +++ b/integration-tests/lakehouse/dev-env-config.json @@ -0,0 +1,3 @@ +{ + "variables": [] +} diff --git a/integration-tests/lakehouse/hop-config.json b/integration-tests/lakehouse/hop-config.json new file mode 100644 index 00000000000..d9e1e6562e0 --- /dev/null +++ b/integration-tests/lakehouse/hop-config.json @@ -0,0 +1,290 @@ +{ + "variables": [ + { + "name": "HOP_LENIENT_STRING_TO_NUMBER_CONVERSION", + "value": "N", + "description": "System wide flag to allow lenient string to number conversion for backward compatibility. If this setting is set to \"Y\", an string starting with digits will be converted successfully into a number. (example: 192.168.1.1 will be converted into 192 or 192.168 or 192168 depending on the decimal and grouping symbol). The default (N) will be to throw an error if non-numeric symbols are found in the string." + }, + { + "name": "HOP_COMPATIBILITY_DB_IGNORE_TIMEZONE", + "value": "N", + "description": "System wide flag to ignore timezone while writing date/timestamp value to the database." + }, + { + "name": "HOP_LOG_SIZE_LIMIT", + "value": "0", + "description": "The log size limit for all pipelines and workflows that don't have the \"log size limit\" property set in their respective properties." + }, + { + "name": "HOP_EMPTY_STRING_DIFFERS_FROM_NULL", + "value": "N", + "description": "NULL vs Empty String. If this setting is set to Y, an empty string and null are different. Otherwise they are not." + }, + { + "name": "HOP_MAX_LOG_SIZE_IN_LINES", + "value": "0", + "description": "The maximum number of log lines that are kept internally by Hop. Set to 0 to keep all rows (default)" + }, + { + "name": "HOP_MAX_LOG_TIMEOUT_IN_MINUTES", + "value": "1440", + "description": "The maximum age (in minutes) of a log line while being kept internally by Hop. Set to 0 to keep all rows indefinitely (default)" + }, + { + "name": "HOP_MAX_WORKFLOW_TRACKER_SIZE", + "value": "5000", + "description": "The maximum number of workflow trackers kept in memory" + }, + { + "name": "HOP_MAX_ACTIONS_LOGGED", + "value": "5000", + "description": "The maximum number of action results kept in memory for logging purposes." + }, + { + "name": "HOP_MAX_LOGGING_REGISTRY_SIZE", + "value": "10000", + "description": "The maximum number of logging registry entries kept in memory for logging purposes." + }, + { + "name": "HOP_LOG_TAB_REFRESH_DELAY", + "value": "1000", + "description": "The hop log tab refresh delay." + }, + { + "name": "HOP_LOG_TAB_REFRESH_PERIOD", + "value": "1000", + "description": "The hop log tab refresh period." + }, + { + "name": "HOP_PLUGIN_CLASSES", + "value": null, + "description": "A comma delimited list of classes to scan for plugin annotations" + }, + { + "name": "HOP_PLUGIN_PACKAGES", + "value": null, + "description": "A comma delimited list of packages to scan for plugin annotations (warning: slow!!)" + }, + { + "name": "HOP_TRANSFORM_PERFORMANCE_SNAPSHOT_LIMIT", + "value": "0", + "description": "The maximum number of transform performance snapshots to keep in memory. Set to 0 to keep all snapshots indefinitely (default)" + }, + { + "name": "HOP_ROWSET_GET_TIMEOUT", + "value": "50", + "description": "The name of the variable that optionally contains an alternative rowset get timeout (in ms). This only makes a difference for extremely short lived pipelines." + }, + { + "name": "HOP_ROWSET_PUT_TIMEOUT", + "value": "50", + "description": "The name of the variable that optionally contains an alternative rowset put timeout (in ms). This only makes a difference for extremely short lived pipelines." + }, + { + "name": "HOP_CORE_TRANSFORMS_FILE", + "value": null, + "description": "The name of the project variable that will contain the alternative location of the hop-transforms.xml file. You can use this to customize the list of available internal transforms outside of the codebase." + }, + { + "name": "HOP_CORE_WORKFLOW_ACTIONS_FILE", + "value": null, + "description": "The name of the project variable that will contain the alternative location of the hop-workflow-actions.xml file." + }, + { + "name": "HOP_SERVER_OBJECT_TIMEOUT_MINUTES", + "value": "1440", + "description": "This project variable will set a time-out after which waiting, completed or stopped pipelines and workflows will be automatically cleaned up. The default value is 1440 (one day)." + }, + { + "name": "HOP_PIPELINE_PAN_JVM_EXIT_CODE", + "value": null, + "description": "Set this variable to an integer that will be returned as the Pan JVM exit code." + }, + { + "name": "HOP_DISABLE_CONSOLE_LOGGING", + "value": "N", + "description": "Set this variable to Y to disable standard Hop logging to the console. (stdout)" + }, + { + "name": "HOP_REDIRECT_STDERR", + "value": "N", + "description": "Set this variable to Y to redirect stderr to Hop logging." + }, + { + "name": "HOP_REDIRECT_STDOUT", + "value": "N", + "description": "Set this variable to Y to redirect stdout to Hop logging." + }, + { + "name": "HOP_DEFAULT_NUMBER_FORMAT", + "value": null, + "description": "The name of the variable containing an alternative default number format" + }, + { + "name": "HOP_DEFAULT_BIGNUMBER_FORMAT", + "value": null, + "description": "The name of the variable containing an alternative default bignumber format" + }, + { + "name": "HOP_DEFAULT_INTEGER_FORMAT", + "value": null, + "description": "The name of the variable containing an alternative default integer format" + }, + { + "name": "HOP_DEFAULT_DATE_FORMAT", + "value": null, + "description": "The name of the variable containing an alternative default date format" + }, + { + "name": "HOP_DEFAULT_TIMESTAMP_FORMAT", + "value": null, + "description": "The name of the variable containing an alternative default timestamp format" + }, + { + "name": "HOP_DEFAULT_SERVLET_ENCODING", + "value": null, + "description": "Defines the default encoding for servlets, leave it empty to use Java default encoding" + }, + { + "name": "HOP_FAIL_ON_LOGGING_ERROR", + "value": "N", + "description": "Set this variable to Y when you want the workflow/pipeline fail with an error when the related logging process (e.g. to a database) fails." + }, + { + "name": "HOP_AGGREGATION_MIN_NULL_IS_VALUED", + "value": "N", + "description": "Set this variable to Y to set the minimum to NULL if NULL is within an aggregate. Otherwise by default NULL is ignored by the MIN aggregate and MIN is set to the minimum value that is not NULL. See also the variable HOP_AGGREGATION_ALL_NULLS_ARE_ZERO." + }, + { + "name": "HOP_AGGREGATION_ALL_NULLS_ARE_ZERO", + "value": "N", + "description": "Set this variable to Y to return 0 when all values within an aggregate are NULL. Otherwise by default a NULL is returned when all values are NULL." + }, + { + "name": "HOP_COMPATIBILITY_TEXT_FILE_OUTPUT_APPEND_NO_HEADER", + "value": "N", + "description": "Set this variable to Y for backward compatibility for the Text File Output transform. Setting this to Ywill add no header row at all when the append option is enabled, regardless if the file is existing or not." + }, + { + "name": "HOP_PASSWORD_ENCODER_PLUGIN", + "value": "Hop", + "description": "Specifies the password encoder plugin to use by ID (Hop is the default)." + }, + { + "name": "HOP_SYSTEM_HOSTNAME", + "value": null, + "description": "You can use this variable to speed up hostname lookup. Hostname lookup is performed by Hop so that it is capable of logging the server on which a workflow or pipeline is executed." + }, + { + "name": "HOP_SERVER_JETTY_ACCEPTORS", + "value": null, + "description": "A variable to configure jetty option: acceptors for Carte" + }, + { + "name": "HOP_SERVER_JETTY_ACCEPT_QUEUE_SIZE", + "value": null, + "description": "A variable to configure jetty option: acceptQueueSize for Carte" + }, + { + "name": "HOP_SERVER_JETTY_RES_MAX_IDLE_TIME", + "value": null, + "description": "A variable to configure jetty option: lowResourcesMaxIdleTime for Carte" + }, + { + "name": "HOP_COMPATIBILITY_MERGE_ROWS_USE_REFERENCE_STREAM_WHEN_IDENTICAL", + "value": "N", + "description": "Set this variable to Y for backward compatibility for the Merge Rows (diff) transform. Setting this to Y will use the data from the reference stream (instead of the comparison stream) in case the compared rows are identical." + }, + { + "name": "HOP_SPLIT_FIELDS_REMOVE_ENCLOSURE", + "value": "false", + "description": "Set this variable to false to preserve enclosure symbol after splitting the string in the Split fields transform. Changing it to true will remove first and last enclosure symbol from the resulting string chunks." + }, + { + "name": "HOP_ALLOW_EMPTY_FIELD_NAMES_AND_TYPES", + "value": "false", + "description": "Set this variable to TRUE to allow your pipeline to pass 'null' fields and/or empty types." + }, + { + "name": "HOP_GLOBAL_LOG_VARIABLES_CLEAR_ON_EXPORT", + "value": "false", + "description": "Set this variable to false to preserve global log variables defined in pipeline / workflow Properties -> Log panel. Changing it to true will clear it when export pipeline / workflow." + }, + { + "name": "HOP_FILE_OUTPUT_MAX_STREAM_COUNT", + "value": "1024", + "description": "This project variable is used by the Text File Output transform. It defines the max number of simultaneously open files within the transform. The transform will close/reopen files as necessary to insure the max is not exceeded" + }, + { + "name": "HOP_FILE_OUTPUT_MAX_STREAM_LIFE", + "value": "0", + "description": "This project variable is used by the Text File Output transform. It defines the max number of milliseconds between flushes of files opened by the transform." + }, + { + "name": "HOP_USE_NATIVE_FILE_DIALOG", + "value": "N", + "description": "Set this value to Y if you want to use the system file open/save dialog when browsing files" + }, + { + "name": "HOP_AUTO_CREATE_CONFIG", + "value": "Y", + "description": "Set this value to N if you don't want to automatically create a hop configuration file (hop-config.json) when it's missing" + } + ], + "LocaleDefault": "en_BE", + "guiProperties": { + "FontFixedSize": "13", + "MaxUndo": "100", + "DarkMode": "Y", + "FontNoteSize": "13", + "ShowOSLook": "Y", + "FontFixedStyle": "0", + "FontNoteName": ".AppleSystemUIFont", + "FontFixedName": "Monospaced", + "FontGraphStyle": "0", + "FontDefaultSize": "13", + "GraphColorR": "255", + "FontGraphSize": "13", + "IconSize": "32", + "BackgroundColorB": "255", + "FontNoteStyle": "0", + "FontGraphName": ".AppleSystemUIFont", + "FontDefaultName": ".AppleSystemUIFont", + "GraphColorG": "255", + "UseGlobalFileBookmarks": "Y", + "FontDefaultStyle": "0", + "GraphColorB": "255", + "BackgroundColorR": "255", + "BackgroundColorG": "255", + "WorkflowDialogStyle": "RESIZE,MAX,MIN", + "LineWidth": "1", + "ContextDialogShowCategories": "Y" + }, + "projectsConfig": { + "enabled": true, + "projectMandatory": true, + "environmentMandatory": false, + "defaultProject": "default", + "defaultEnvironment": null, + "standardParentProject": "default", + "standardProjectsFolder": null, + "projectConfigurations": [ + { + "projectName": "default", + "projectHome": "${HOP_CONFIG_FOLDER}", + "configFilename": "project-config.json" + } + ], + "lifecycleEnvironments": [ + { + "name": "dev", + "purpose": "Testing", + "projectName": "default", + "configurationFiles": [ + "${PROJECT_HOME}/dev-env-config.json" + ] + } + ], + "projectLifecycles": [] + } +} \ No newline at end of file diff --git a/integration-tests/lakehouse/main-0001-input-output.hwf b/integration-tests/lakehouse/main-0001-input-output.hwf new file mode 100644 index 00000000000..1c06fe24c67 --- /dev/null +++ b/integration-tests/lakehouse/main-0001-input-output.hwf @@ -0,0 +1,180 @@ + + + + main-0001-input-output + Y + delta Lake Table Input/Output PATH round-trip + + + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + Start + + SPECIAL + + N + 0 + 0 + 60 + 12 + 0 + 1 + 1 + N + 64 + 96 + + + + clean work dir + Remove previous table/extract data under /tmp/hop-it-lakehouse (always succeeds) + SHELL + + N + N + N + + + N + Y + Basic + + N + N + N + 224 + 96 + + + + 0001-write + + PIPELINE + + ${PROJECT_HOME}/0001-write.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + + Y + + N + 400 + 96 + + + + 0001-read-extract + + PIPELINE + + ${PROJECT_HOME}/0001-read-extract.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + + Y + + N + 576 + 96 + + + + Run unit tests + + RunPipelineTests + + + + 0001-validation UNIT + + + N + 752 + 96 + + + + + + Start + clean work dir + Y + Y + Y + + + clean work dir + 0001-write + Y + Y + N + + + 0001-write + 0001-read-extract + Y + Y + N + + + 0001-read-extract + Run unit tests + Y + Y + N + + + + + diff --git a/integration-tests/lakehouse/main-0002-overwrite-timetravel.hwf b/integration-tests/lakehouse/main-0002-overwrite-timetravel.hwf new file mode 100644 index 00000000000..c28a9b80d81 --- /dev/null +++ b/integration-tests/lakehouse/main-0002-overwrite-timetravel.hwf @@ -0,0 +1,253 @@ + + + + main-0002-overwrite-timetravel + Y + delta overwrite + VERSION 0 time travel + + + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + Start + + SPECIAL + + N + 0 + 0 + 60 + 12 + 0 + 1 + 1 + N + 64 + 96 + + + + clean work dir + Remove previous table/extract data under /tmp/hop-it-lakehouse (always succeeds) + SHELL + + N + N + N + + + N + Y + Basic + + N + N + N + 224 + 96 + + + + write v1 + + PIPELINE + + ${PROJECT_HOME}/0002-write-v1.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + + Y + + N + 360 + 96 + + + + write v2 + + PIPELINE + + ${PROJECT_HOME}/0002-write-v2.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + + Y + + N + 520 + 96 + + + + read current + + PIPELINE + + ${PROJECT_HOME}/0002-read-current.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + + Y + + N + 680 + 96 + + + + read asof + + PIPELINE + + ${PROJECT_HOME}/0002-read-asof.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + + Y + + N + 840 + 96 + + + + Run unit tests + + RunPipelineTests + + + + 0002-validation-current UNIT + + + 0002-validation-asof UNIT + + + N + 1000 + 96 + + + + + + Start + clean work dir + Y + Y + Y + + + clean work dir + write v1 + Y + Y + N + + + write v1 + write v2 + Y + Y + N + + + write v2 + read current + Y + Y + N + + + read current + read asof + Y + Y + N + + + read asof + Run unit tests + Y + Y + N + + + + + diff --git a/integration-tests/lakehouse/main-0003-merge.hwf b/integration-tests/lakehouse/main-0003-merge.hwf new file mode 100644 index 00000000000..226f5b8839b --- /dev/null +++ b/integration-tests/lakehouse/main-0003-merge.hwf @@ -0,0 +1,215 @@ + + + + main-0003-merge + Y + delta Lake Table MERGE upsert + + + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + Start + + SPECIAL + + N + 0 + 0 + 60 + 12 + 0 + 1 + 1 + N + 64 + 96 + + + + clean work dir + Remove previous table/extract data under /tmp/hop-it-lakehouse (always succeeds) + SHELL + + N + N + N + + + N + Y + Basic + + N + N + N + 224 + 96 + + + + seed + + PIPELINE + + ${PROJECT_HOME}/0003-seed.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + + Y + + N + 400 + 96 + + + + merge + + PIPELINE + + ${PROJECT_HOME}/0003-merge.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + + Y + + N + 560 + 96 + + + + read + + PIPELINE + + ${PROJECT_HOME}/0003-read-extract.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + + Y + + N + 720 + 96 + + + + Run unit tests + + RunPipelineTests + + + + 0003-validation UNIT + + + N + 880 + 96 + + + + + + Start + clean work dir + Y + Y + Y + + + clean work dir + seed + Y + Y + N + + + seed + merge + Y + Y + N + + + merge + read + Y + Y + N + + + read + Run unit tests + Y + Y + N + + + + + diff --git a/integration-tests/lakehouse/metadata/dataset/0001-roundtrip-golden.json b/integration-tests/lakehouse/metadata/dataset/0001-roundtrip-golden.json new file mode 100644 index 00000000000..a1e025d75b6 --- /dev/null +++ b/integration-tests/lakehouse/metadata/dataset/0001-roundtrip-golden.json @@ -0,0 +1,24 @@ +{ + "base_filename": "0001-roundtrip-golden.csv", + "name": "0001-roundtrip-golden", + "description": "10-row PATH round-trip", + "dataset_fields": [ + { + "field_comment": "", + "field_length": -1, + "field_type": 5, + "field_precision": 0, + "field_name": "id", + "field_format": "#" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "name", + "field_format": "" + } + ], + "folder_name": "" +} diff --git a/integration-tests/lakehouse/metadata/dataset/0002-asof-golden.json b/integration-tests/lakehouse/metadata/dataset/0002-asof-golden.json new file mode 100644 index 00000000000..dae2f844858 --- /dev/null +++ b/integration-tests/lakehouse/metadata/dataset/0002-asof-golden.json @@ -0,0 +1,24 @@ +{ + "base_filename": "0002-asof-golden.csv", + "name": "0002-asof-golden", + "description": "Time travel first write (5 rows)", + "dataset_fields": [ + { + "field_comment": "", + "field_length": -1, + "field_type": 5, + "field_precision": 0, + "field_name": "id", + "field_format": "#" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "name", + "field_format": "" + } + ], + "folder_name": "" +} diff --git a/integration-tests/lakehouse/metadata/dataset/0002-current-golden.json b/integration-tests/lakehouse/metadata/dataset/0002-current-golden.json new file mode 100644 index 00000000000..432e22ed988 --- /dev/null +++ b/integration-tests/lakehouse/metadata/dataset/0002-current-golden.json @@ -0,0 +1,24 @@ +{ + "base_filename": "0002-current-golden.csv", + "name": "0002-current-golden", + "description": "After second overwrite (current)", + "dataset_fields": [ + { + "field_comment": "", + "field_length": -1, + "field_type": 5, + "field_precision": 0, + "field_name": "id", + "field_format": "#" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "name", + "field_format": "" + } + ], + "folder_name": "" +} diff --git a/integration-tests/lakehouse/metadata/dataset/0003-merge-golden.json b/integration-tests/lakehouse/metadata/dataset/0003-merge-golden.json new file mode 100644 index 00000000000..561a4c69a00 --- /dev/null +++ b/integration-tests/lakehouse/metadata/dataset/0003-merge-golden.json @@ -0,0 +1,24 @@ +{ + "base_filename": "0003-merge-golden.csv", + "name": "0003-merge-golden", + "description": "After MERGE upsert", + "dataset_fields": [ + { + "field_comment": "", + "field_length": -1, + "field_type": 5, + "field_precision": 0, + "field_name": "id", + "field_format": "#" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "name", + "field_format": "" + } + ], + "folder_name": "" +} diff --git a/integration-tests/lakehouse/metadata/pipeline-run-configuration/local.json b/integration-tests/lakehouse/metadata/pipeline-run-configuration/local.json new file mode 100644 index 00000000000..76f9ec72c22 --- /dev/null +++ b/integration-tests/lakehouse/metadata/pipeline-run-configuration/local.json @@ -0,0 +1,17 @@ +{ + "engineRunConfiguration": { + "Local": { + "feedback_size": "50000", + "sample_size": "100", + "sample_type_in_gui": "Last", + "rowset_size": "10000", + "safe_mode": false, + "show_feedback": false, + "topo_sort": false, + "gather_metrics": false + } + }, + "configurationVariables": [], + "name": "local", + "description": "Local Hop pipeline engine for validation" +} diff --git a/integration-tests/lakehouse/metadata/pipeline-run-configuration/spark-local.json b/integration-tests/lakehouse/metadata/pipeline-run-configuration/spark-local.json new file mode 100644 index 00000000000..ff481ecdfda --- /dev/null +++ b/integration-tests/lakehouse/metadata/pipeline-run-configuration/spark-local.json @@ -0,0 +1,18 @@ +{ + "engineRunConfiguration": { + "SparkPipelineEngine": { + "sparkMaster": "local[2]", + "sparkAppName": "hop-it-lakehouse", + "sparkConfigs": "spark.ui.enabled=false\nspark.driver.host=localhost", + "fatJar": "", + "tempLocation": "/tmp", + "driverMemory": "", + "executorMemory": "", + "executorCores": "", + "pluginsToStage": "" + } + }, + "configurationVariables": [], + "name": "spark-local", + "description": "Native Spark local[2] for lakehouse lake table ITs" +} diff --git a/integration-tests/lakehouse/metadata/unit-test/0001-validation UNIT.json b/integration-tests/lakehouse/metadata/unit-test/0001-validation UNIT.json new file mode 100644 index 00000000000..3c4c89d8a6d --- /dev/null +++ b/integration-tests/lakehouse/metadata/unit-test/0001-validation UNIT.json @@ -0,0 +1,32 @@ +{ + "variableValues": [], + "database_replacements": [], + "autoOpening": true, + "basePath": "", + "golden_data_sets": [ + { + "field_mappings": [ + { + "transform_field": "id", + "data_set_field": "id" + }, + { + "transform_field": "name", + "data_set_field": "name" + } + ], + "field_order": [ + "id" + ], + "data_set_name": "0001-roundtrip-golden", + "transform_name": "Verify" + } + ], + "input_data_sets": [], + "name": "0001-validation UNIT", + "description": "Golden compare for PATH round-trip", + "persist_filename": "", + "trans_test_tweaks": [], + "pipeline_filename": "./0001-validation.hpl", + "test_type": "UNIT_TEST" +} diff --git a/integration-tests/lakehouse/metadata/unit-test/0002-validation-asof UNIT.json b/integration-tests/lakehouse/metadata/unit-test/0002-validation-asof UNIT.json new file mode 100644 index 00000000000..f2c72c45ca7 --- /dev/null +++ b/integration-tests/lakehouse/metadata/unit-test/0002-validation-asof UNIT.json @@ -0,0 +1,32 @@ +{ + "variableValues": [], + "database_replacements": [], + "autoOpening": true, + "basePath": "", + "golden_data_sets": [ + { + "field_mappings": [ + { + "transform_field": "id", + "data_set_field": "id" + }, + { + "transform_field": "name", + "data_set_field": "name" + } + ], + "field_order": [ + "id" + ], + "data_set_name": "0002-asof-golden", + "transform_name": "Verify" + } + ], + "input_data_sets": [], + "name": "0002-validation-asof UNIT", + "description": "Time travel first version", + "persist_filename": "", + "trans_test_tweaks": [], + "pipeline_filename": "./0002-validation-asof.hpl", + "test_type": "UNIT_TEST" +} diff --git a/integration-tests/lakehouse/metadata/unit-test/0002-validation-current UNIT.json b/integration-tests/lakehouse/metadata/unit-test/0002-validation-current UNIT.json new file mode 100644 index 00000000000..1018f4be8bc --- /dev/null +++ b/integration-tests/lakehouse/metadata/unit-test/0002-validation-current UNIT.json @@ -0,0 +1,32 @@ +{ + "variableValues": [], + "database_replacements": [], + "autoOpening": true, + "basePath": "", + "golden_data_sets": [ + { + "field_mappings": [ + { + "transform_field": "id", + "data_set_field": "id" + }, + { + "transform_field": "name", + "data_set_field": "name" + } + ], + "field_order": [ + "id" + ], + "data_set_name": "0002-current-golden", + "transform_name": "Verify" + } + ], + "input_data_sets": [], + "name": "0002-validation-current UNIT", + "description": "Current table after second overwrite", + "persist_filename": "", + "trans_test_tweaks": [], + "pipeline_filename": "./0002-validation-current.hpl", + "test_type": "UNIT_TEST" +} diff --git a/integration-tests/lakehouse/metadata/unit-test/0003-validation UNIT.json b/integration-tests/lakehouse/metadata/unit-test/0003-validation UNIT.json new file mode 100644 index 00000000000..8c61cf93665 --- /dev/null +++ b/integration-tests/lakehouse/metadata/unit-test/0003-validation UNIT.json @@ -0,0 +1,32 @@ +{ + "variableValues": [], + "database_replacements": [], + "autoOpening": true, + "basePath": "", + "golden_data_sets": [ + { + "field_mappings": [ + { + "transform_field": "id", + "data_set_field": "id" + }, + { + "transform_field": "name", + "data_set_field": "name" + } + ], + "field_order": [ + "id" + ], + "data_set_name": "0003-merge-golden", + "transform_name": "Verify" + } + ], + "input_data_sets": [], + "name": "0003-validation UNIT", + "description": "MERGE upsert golden", + "persist_filename": "", + "trans_test_tweaks": [], + "pipeline_filename": "./0003-validation.hpl", + "test_type": "UNIT_TEST" +} diff --git a/integration-tests/lakehouse/metadata/workflow-run-configuration/local.json b/integration-tests/lakehouse/metadata/workflow-run-configuration/local.json new file mode 100644 index 00000000000..911da1e1fe6 --- /dev/null +++ b/integration-tests/lakehouse/metadata/workflow-run-configuration/local.json @@ -0,0 +1,9 @@ +{ + "engineRunConfiguration": { + "Local": { + "safe_mode": false + } + }, + "name": "local", + "description": "Local workflow engine" +} diff --git a/integration-tests/lakehouse/output/.gitkeep b/integration-tests/lakehouse/output/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/integration-tests/lakehouse/project-config.json b/integration-tests/lakehouse/project-config.json new file mode 100644 index 00000000000..3b7af5d8b73 --- /dev/null +++ b/integration-tests/lakehouse/project-config.json @@ -0,0 +1,15 @@ +{ + "metadataBaseFolder": "${PROJECT_HOME}/metadata", + "unitTestsBasePath": "${PROJECT_HOME}", + "dataSetsCsvFolder": "${PROJECT_HOME}/datasets", + "enforcingExecutionInHome": true, + "config": { + "variables": [ + { + "name": "HOP_LICENSE_HEADER_FILE", + "value": "${PROJECT_HOME}/../asf-header.txt", + "description": "ASF license header for serialized pipelines/workflows" + } + ] + } +} diff --git a/integration-tests/scripts/run-tests-docker.sh b/integration-tests/scripts/run-tests-docker.sh index 2d095aad192..1d56361d104 100755 --- a/integration-tests/scripts/run-tests-docker.sh +++ b/integration-tests/scripts/run-tests-docker.sh @@ -79,22 +79,29 @@ if [ -z "${TEST_FILTER}" ]; then fi export TEST_FILTER +# Match the host/workspace owner when not overridden. ASF Jenkins (Jenkinsfile.daily) +# always passes the agent identity explicitly: +# JENKINS_USER=${USER} JENKINS_UID=$(id -u) JENKINS_GROUP=$(id -gn) JENKINS_GID=$(id -g) +# Using the same defaults locally keeps the container user aligned with the bind-mounted +# integration-tests/ tree, so writes under ${PROJECT_HOME}/output (and elsewhere) succeed. if [ -z "${JENKINS_USER}" ]; then - JENKINS_USER="jenkins" + JENKINS_USER="$(id -un 2>/dev/null || echo jenkins)" fi if [ -z "${JENKINS_UID}" ]; then - JENKINS_UID="1001" + JENKINS_UID="$(id -u 2>/dev/null || echo 1000)" fi if [ -z "${JENKINS_GROUP}" ]; then - JENKINS_GROUP="jenkins" + JENKINS_GROUP="$(id -gn 2>/dev/null || echo jenkins)" fi if [ -z "${JENKINS_GID}" ]; then - JENKINS_GID="1001" + JENKINS_GID="$(id -g 2>/dev/null || echo 1000)" fi +echo "Integration-test container identity: user=${JENKINS_USER} uid=${JENKINS_UID} group=${JENKINS_GROUP} gid=${JENKINS_GID}" + if [ -z "${SUREFIRE_REPORT}" ]; then SUREFIRE_REPORT="true" fi @@ -103,6 +110,17 @@ if [ -z "${GCP_KEY_FILE}" ]; then GCP_KEY_FILE="./docker/integration-tests/resource/dummyfile" fi +# Detect a real Google Cloud service-account key. The dummy file is a license comment, not +# JSON; spreadsheet Google Sheets ITs need a real key (Jenkins: credentials gcp-access-hop). +SKIP_GOOGLE_SHEETS="false" +if [ ! -f "${GCP_KEY_FILE}" ] \ + || [[ "${GCP_KEY_FILE}" == *dummyfile* ]] \ + || ! grep -qE '"type"[[:space:]]*:[[:space:]]*"service_account"' "${GCP_KEY_FILE}" 2>/dev/null; then + SKIP_GOOGLE_SHEETS="true" + echo "No valid GCP service-account JSON at GCP_KEY_FILE=${GCP_KEY_FILE}; spreadsheet Google Sheets tests will be skipped" +fi +export SKIP_GOOGLE_SHEETS + if [ -z "${HOP_OPTIONS}" ] ; then HOP_OPTIONS="${HOP_OPTIONS} -Djavax.net.ssl.keyStore=./docker/integration-tests/resource/keystore.jks -Djavax.net.ssl.keyStorePassword=password -Djavax.net.ssl.trustStore=./docker/integration-tests/resource/mail/conf/keystore " fi @@ -124,6 +142,47 @@ fi mkdir -p "${CURRENT_DIR}"/../surefire-reports/ chmod 777 "${CURRENT_DIR}"/../surefire-reports/ +# Pre-create project output/ dirs on the host and make them world-writable. +# ASF Jenkins passes a container UID that matches the agent workspace owner, so ownership +# alone is enough there. World-writable output/ is a belt-and-suspenders for: +# - local runs where someone overrides JENKINS_UID to a fixed value +# - compose files that hardcode build-arg UIDs when not using this script's --build-arg +# Pipelines that write temp artifacts (Excel/ODS writer, Spark CSV, etc.) use +# ${PROJECT_HOME}/output. +# +# Spark/Spark-native leave untracked part-*.csv / .crc trees under output// owned by +# the previous container UID (often 755). Later runs with a different JENKINS_UID cannot +# delete them. Only remove *untracked* residual files — never wipe git-tracked content +# (ldap stores setup pipelines under output/*.hpl). +REPO_ROOT="$(cd "${CURRENT_DIR}/../.." && pwd)" +for d in "${CURRENT_DIR}"/../${PROJECT_NAME}/; do + if [[ "$d" != *"scripts/" ]] && [[ "$d" != *"surefire-reports/" ]] && [[ "$d" != *"hopweb/" ]]; then + if [ -d "$d" ] && [ ! -f "$d/disabled.txt" ]; then + mkdir -p "$d/output" + chmod 777 "$d/output" 2>/dev/null || true + abs_out="$(cd "$d/output" && pwd)" + rel_out="${abs_out#"${REPO_ROOT}"/}" + + # 1) Untracked leftovers only when the host can delete them (safe for ldap/*.hpl). + git -C "${REPO_ROOT}" clean -fd -- "${rel_out}" 2>/dev/null || true + + # 2) Other-UID Spark residuals: chmod as root, then host git clean again. + # Never blanket-delete under output/ (that wiped ldap/output/*.hpl). + if find "$d/output" \( -name 'part-*' -o -name '_SUCCESS' -o -name '*.crc' \) 2>/dev/null | grep -q .; then + echo "Fixing permissions on Spark residual files under ${rel_out}" + docker run --rm -v "${abs_out}:/out" alpine:3.19 \ + sh -c 'chmod -R a+rwx /out 2>/dev/null; true' 2>/dev/null || true + git -C "${REPO_ROOT}" clean -fd -- "${rel_out}" 2>/dev/null || true + # Pattern-based delete for anything still stuck (Spark artifacts only) + docker run --rm -v "${abs_out}:/out" alpine:3.19 \ + sh -c 'find /out \( -name "part-*" -o -name "_SUCCESS" -o -name "*.crc" \) -exec rm -rf {} + 2>/dev/null; find /out -type d -empty -delete 2>/dev/null; true' \ + 2>/dev/null || true + fi + chmod 777 "$d/output" 2>/dev/null || true + fi + fi +done + HOP_CLIENT_TARGET_DIR="${CURRENT_DIR}/../../assemblies/client/target" HOP_DIR="${HOP_CLIENT_TARGET_DIR}/hop" @@ -225,17 +284,26 @@ for d in "${CURRENT_DIR}"/../${PROJECT_NAME}/; do if [ -f "${DOCKER_FILES_DIR}/integration-tests-${PROJECT_NAME}.yaml" ]; then echo "Project compose exists." EXECUTED_COMPOSE_FILES=("${EXECUTED_COMPOSE_FILES[@]}" "${DOCKER_FILES_DIR}/integration-tests-${PROJECT_NAME}.yaml") - # Rebuild project images so SPARK_VERSION (and similar) build args take effect + # Rebuild project images so SPARK_VERSION (and similar) build args take effect. + # hop_server also must rebuild: its hop-server service image (apache/hop:Development + # from docker/Dockerfile) otherwise stays cached and can miss client-side assembly + # plugins needed by remote-export ITs (main-0008/0009/0010). if [ "${PROJECT_NAME}" = "spark" ]; then echo "Spark IT cluster version: ${SPARK_VERSION} (hadoop ${HADOOP_VERSION})" - PROJECT_NAME=${PROJECT_NAME} TEST_FILTER=${TEST_FILTER} SPARK_VERSION=${SPARK_VERSION} HADOOP_VERSION=${HADOOP_VERSION} SPARK_BASE_URL=${SPARK_BASE_URL} \ + PROJECT_NAME=${PROJECT_NAME} TEST_FILTER=${TEST_FILTER} SKIP_GOOGLE_SHEETS=${SKIP_GOOGLE_SHEETS} SPARK_VERSION=${SPARK_VERSION} HADOOP_VERSION=${HADOOP_VERSION} SPARK_BASE_URL=${SPARK_BASE_URL} \ + docker compose -f ${DOCKER_FILES_DIR}/integration-tests-${PROJECT_NAME}.yaml up --build --abort-on-container-exit + elif [ "${PROJECT_NAME}" = "hop_server" ]; then + echo "Rebuilding hop_server images so remote Hop Server matches current assemblies" + PROJECT_NAME=${PROJECT_NAME} TEST_FILTER=${TEST_FILTER} SKIP_GOOGLE_SHEETS=${SKIP_GOOGLE_SHEETS} \ docker compose -f ${DOCKER_FILES_DIR}/integration-tests-${PROJECT_NAME}.yaml up --build --abort-on-container-exit else - PROJECT_NAME=${PROJECT_NAME} TEST_FILTER=${TEST_FILTER} docker compose -f ${DOCKER_FILES_DIR}/integration-tests-${PROJECT_NAME}.yaml up --abort-on-container-exit + PROJECT_NAME=${PROJECT_NAME} TEST_FILTER=${TEST_FILTER} SKIP_GOOGLE_SHEETS=${SKIP_GOOGLE_SHEETS} \ + docker compose -f ${DOCKER_FILES_DIR}/integration-tests-${PROJECT_NAME}.yaml up --abort-on-container-exit fi else echo "Project compose does not exists." - PROJECT_NAME=${PROJECT_NAME} TEST_FILTER=${TEST_FILTER} docker compose -f ${DOCKER_FILES_DIR}/integration-tests-base.yaml up --abort-on-container-exit + PROJECT_NAME=${PROJECT_NAME} TEST_FILTER=${TEST_FILTER} SKIP_GOOGLE_SHEETS=${SKIP_GOOGLE_SHEETS} \ + docker compose -f ${DOCKER_FILES_DIR}/integration-tests-base.yaml up --abort-on-container-exit fi fi fi @@ -261,13 +329,16 @@ if [ ! "${KEEP_IMAGES}" = "true" ]; then fi # Print Final Results +# Use CURRENT_DIR (script location) for both existence checks and reads. Relative +# paths like ../surefire-reports/ only work when cwd is integration-tests/scripts/; +# ASF Jenkins and local runs often invoke this script from the repo root. if [ -f "${CURRENT_DIR}/../surefire-reports/passed_tests" ]; then echo -e "\033[1;32mPassed tests:" - PASSED_TESTS="$(cat ../surefire-reports/passed_tests)" + PASSED_TESTS="$(cat "${CURRENT_DIR}/../surefire-reports/passed_tests")" echo -e "\033[1;32m${PASSED_TESTS}" fi if [ -f "${CURRENT_DIR}/../surefire-reports/failed_tests" ]; then echo -e "\033[1;91mFailed tests:" - FAILED_TESTS="$(cat ../surefire-reports/failed_tests)" + FAILED_TESTS="$(cat "${CURRENT_DIR}/../surefire-reports/failed_tests")" echo -e "\033[1;91m${FAILED_TESTS}" fi diff --git a/integration-tests/scripts/run-tests.sh b/integration-tests/scripts/run-tests.sh index 6db92c699cb..a63d2e8933c 100755 --- a/integration-tests/scripts/run-tests.sh +++ b/integration-tests/scripts/run-tests.sh @@ -130,6 +130,12 @@ if [ -z "${TEST_FILTER}" ]; then TEST_FILTER="" fi +# When the GCP service-account key is missing or is the IT dummy file, skip Google Sheets +# workflows (they need a real JSON key; ASF Jenkins provides credentials id gcp-access-hop). +if [ -z "${SKIP_GOOGLE_SHEETS}" ]; then + SKIP_GOOGLE_SHEETS="false" +fi + #set global variables SPACER="===========================================" @@ -139,6 +145,15 @@ should_run_workflow() { local base base=$(basename "$file") + if [ "${SKIP_GOOGLE_SHEETS}" = "true" ]; then + case "${base}" in + *google-sheet* | *google-sheets*) + echo "Skipping ${base} (SKIP_GOOGLE_SHEETS=true: no valid GCP service-account JSON)" + return 1 + ;; + esac + fi + if [ -z "${TEST_FILTER}" ]; then return 0 fi @@ -240,6 +255,16 @@ for d in "${CURRENT_DIR}"/../${PROJECT_NAME}/; do # Create New Project export HOP_CONFIG_FOLDER="$d" + # Project output/ is often written by pipelines (CSV, Excel/ODS temp files, etc.). + # On ASF Jenkins the container UID matches the agent workspace owner (Jenkinsfile.daily + # passes id -u / id -g), so writes succeed by ownership. When UIDs differ, output/ is + # pre-created and chmod'd world-writable by run-tests-docker.sh on the host; here we + # only best-effort reinforce that (mkdir/chmod may no-op if not owner). + mkdir -p "$d/output" 2>/dev/null || true + if [ -d "$d/output" ]; then + chmod 777 "$d/output" 2>/dev/null || true + fi + # Default pipeline run configuration name used by hop-run and the suite runner. # Beam projects name their Beam engine "local" and keep a native Local engine as "hop-local". # The single-JVM suite driver (run-project-tests.hpl) must never run under Beam. @@ -250,8 +275,17 @@ for d in "${CURRENT_DIR}"/../${PROJECT_NAME}/; do fi # Prefer single-JVM suite runner when available (unless isolation mode is requested). - # TEST_FILTER is only applied on the classic per-workflow path, so force that mode when set. - if [ "${HOP_IT_PER_TEST_JVM}" != "true" ] && [ -z "${TEST_FILTER}" ] && [ -f "${RUNNER_PIPELINE}" ]; then + # TEST_FILTER and SKIP_GOOGLE_SHEETS are only applied on the classic per-workflow path + # (should_run_workflow), so force that mode when either is set for this project. + USE_SUITE_RUNNER=true + if [ "${HOP_IT_PER_TEST_JVM}" = "true" ] || [ -n "${TEST_FILTER}" ]; then + USE_SUITE_RUNNER=false + fi + if [ "${SKIP_GOOGLE_SHEETS}" = "true" ] && [ "${PROJECT_NAME}" = "spreadsheet" ]; then + USE_SUITE_RUNNER=false + echo "SKIP_GOOGLE_SHEETS=true: using classic per-workflow runner so Google Sheets tests are skipped" + fi + if [ "${USE_SUITE_RUNNER}" = "true" ] && [ -f "${RUNNER_PIPELINE}" ]; then echo ${SPACER} echo "Running project tests in single JVM via run-project-tests.hpl (run config: ${SUITE_RUN_CONFIG})" diff --git a/integration-tests/spark-native/0001-input-output-validation.hpl b/integration-tests/spark-native/0001-input-output-validation.hpl new file mode 100644 index 00000000000..ed5c3f715a8 --- /dev/null +++ b/integration-tests/spark-native/0001-input-output-validation.hpl @@ -0,0 +1,289 @@ + + + + + 0001-input-output-validation + Y + Read Spark File Output CSV on Local and expose Verify for unit test golden + Normal + + N + - + 2026/07/15 00:00:00.000 + - + 2026/07/15 00:00:00.000 + + + + + Read output CSV + Verify + Y + + + + Read output CSV + TextFileInput2 + + Y + + 1 + + none + + + N + N + + + , + " + N + +
Y
+ 1 +
N
+ 1 + N + 1 + N + 80 + 0 + Y + N + + N + N + + Unix + + Characters + Y + + ${PROJECT_HOME}/output/0001-customers-test/ + .*\.csv + + Y + N + CSV + None + + + + + id + Integer + # + + + + + + -1 + -1 + -1 + both + N + + + name + String + + + + + + + -1 + -1 + -1 + both + N + + + firstname + String + + + + + + + -1 + -1 + -1 + both + N + + + zip + String + + + + + + + -1 + -1 + -1 + both + N + + + city + String + + + + + + + -1 + -1 + -1 + both + N + + + birthdate + String + + + + + + + -1 + -1 + -1 + both + N + + + street + String + + + + + + + -1 + -1 + -1 + both + N + + + housenr + String + + + + + + + -1 + -1 + -1 + both + N + + + stateCode + String + + + + + + + -1 + -1 + -1 + both + N + + + state + String + + + + + + + -1 + -1 + -1 + both + N + + + 0 + N + N + + + N + + + + + warning + + error + + line + Y + + + + + + + + + + + + 176 + 128 + +
+ + Verify + Dummy + + Y + + 1 + + none + + + + + 432 + 128 + + + + +
diff --git a/integration-tests/spark-native/0001-input-output.hpl b/integration-tests/spark-native/0001-input-output.hpl new file mode 100644 index 00000000000..734ffec4127 --- /dev/null +++ b/integration-tests/spark-native/0001-input-output.hpl @@ -0,0 +1,152 @@ + + + + + 0001-input-output + Y + Native Spark File Input to File Output (customers-1k) + Normal + + N + - + 2026/07/15 00:00:00.000 + - + 2026/07/15 00:00:00.000 + + + + + Spark File Input + Spark File Output + Y + + + + SparkFileInput + Spark File Input + + Y + 1 + + none + + + ${PROJECT_HOME}/files/customers-1k.txt + csv +
Y
+ ; + " + N + N + + + + id + Integer + 7 + -1 + + + name + String + 50 + -1 + + + firstname + String + 50 + -1 + + + zip + String + 50 + -1 + + + city + String + 50 + -1 + + + birthdate + String + 20 + -1 + + + street + String + 50 + -1 + + + housenr + String + 50 + -1 + + + stateCode + String + 2 + -1 + + + state + String + 50 + -1 + + + + + 160 + 128 + +
+ + SparkFileOutput + Spark File Output + + Y + 1 + + none + + + ${PROJECT_HOME}/output/0001-customers-test + csv + Overwrite +
Y
+ , + " + + 1 + + + + 480 + 128 + +
+ + +
diff --git a/integration-tests/spark-native/0002-group-by-validation.hpl b/integration-tests/spark-native/0002-group-by-validation.hpl new file mode 100644 index 00000000000..18fa41ddb9d --- /dev/null +++ b/integration-tests/spark-native/0002-group-by-validation.hpl @@ -0,0 +1,229 @@ + + + + + 0002-group-by-validation + Y + Read group-by CSV on Local and expose Verify for unit test golden + Normal + + N + - + 2026/07/15 00:00:00.000 + - + 2026/07/15 00:00:00.000 + + + + + Read output CSV + Verify + Y + + + + Read output CSV + TextFileInput2 + + Y + + 1 + + none + + + N + N + + + , + " + N + +
Y
+ 1 +
N
+ 1 + N + 1 + N + 80 + 0 + Y + N + + N + N + + Unix + + Characters + Y + + ${PROJECT_HOME}/output/0002-group-by/ + .*\.csv + + Y + N + CSV + None + + + + + state + String + + + + + + + -1 + -1 + -1 + both + N + + + count_all + Integer + # + + + + + + -1 + -1 + 0 + both + N + + + count_distinct_city + Integer + # + + + + + + -1 + -1 + 0 + both + N + + + min_id + Integer + # + + + + + + -1 + -1 + 0 + both + N + + + max_id + Integer + # + + + + + + -1 + -1 + 0 + both + N + + + sum_id + Integer + # + + + + + + -1 + -1 + 0 + both + N + + + 0 + N + N + + + N + + + + + warning + + error + + line + Y + + + + + + + + + + + + 176 + 128 + +
+ + Verify + Dummy + + Y + + 1 + + none + + + + + 432 + 128 + + + + +
diff --git a/integration-tests/spark-native/0002-group-by.hpl b/integration-tests/spark-native/0002-group-by.hpl new file mode 100644 index 00000000000..dfca64e555e --- /dev/null +++ b/integration-tests/spark-native/0002-group-by.hpl @@ -0,0 +1,211 @@ + + + + + 0002-group-by + Y + Native Spark Memory Group By (state): count all, count distinct city, min/max/sum id + Normal + + N + - + 2026/07/15 00:00:00.000 + - + 2026/07/15 00:00:00.000 + + + + + Spark File Input + Group by state + Y + + + Group by state + Spark File Output + Y + + + + SparkFileInput + Spark File Input + + Y + 1 + + none + + + ${PROJECT_HOME}/files/customers-1k.txt + csv +
Y
+ ; + " + N + N + + + + id + Integer + 7 + -1 + + + name + String + 50 + -1 + + + firstname + String + 50 + -1 + + + zip + String + 50 + -1 + + + city + String + 50 + -1 + + + birthdate + String + 20 + -1 + + + street + String + 50 + -1 + + + housenr + String + 50 + -1 + + + stateCode + String + 2 + -1 + + + state + String + 50 + -1 + + + + + 160 + 128 + +
+ + MemoryGroupBy + Group by state + + Y + 1 + + none + + + N + + + state + + + + + count_all + + COUNT_ANY + + + + count_distinct_city + city + COUNT_DISTINCT + + + + min_id + id + MIN + + + + max_id + id + MAX + + + + sum_id + id + SUM + + + + + + 368 + 128 + + + + SparkFileOutput + Spark File Output + + Y + 1 + + none + + + ${PROJECT_HOME}/output/0002-group-by + csv + Overwrite +
Y
+ , + " + + 1 + + + + 592 + 128 + +
+ + +
diff --git a/integration-tests/spark-native/0003-filter-rows-validation.hpl b/integration-tests/spark-native/0003-filter-rows-validation.hpl new file mode 100644 index 00000000000..5e5347262fe --- /dev/null +++ b/integration-tests/spark-native/0003-filter-rows-validation.hpl @@ -0,0 +1,289 @@ + + + + + 0003-filter-rows-validation + Y + Read filter-rows CSV on Local and expose Verify for unit test golden + Normal + + N + - + 2026/07/15 00:00:00.000 + - + 2026/07/15 00:00:00.000 + + + + + Read output CSV + Verify + Y + + + + Read output CSV + TextFileInput2 + + Y + + 1 + + none + + + N + N + + + , + " + N + +
Y
+ 1 +
N
+ 1 + N + 1 + N + 80 + 0 + Y + N + + N + N + + Unix + + Characters + Y + + ${PROJECT_HOME}/output/0003-filter-rows/ + .*\.csv + + Y + N + CSV + None + + + + + id + Integer + # + + + + + + -1 + -1 + -1 + both + N + + + name + String + + + + + + + -1 + -1 + -1 + both + N + + + firstname + String + + + + + + + -1 + -1 + -1 + both + N + + + zip + String + + + + + + + -1 + -1 + -1 + both + N + + + city + String + + + + + + + -1 + -1 + -1 + both + N + + + birthdate + String + + + + + + + -1 + -1 + -1 + both + N + + + street + String + + + + + + + -1 + -1 + -1 + both + N + + + housenr + String + + + + + + + -1 + -1 + -1 + both + N + + + stateCode + String + + + + + + + -1 + -1 + -1 + both + N + + + state + String + + + + + + + -1 + -1 + -1 + both + N + + + 0 + N + N + + + N + + + + + warning + + error + + line + Y + + + + + + + + + + + + 176 + 128 + +
+ + Verify + Dummy + + Y + + 1 + + none + + + + + 432 + 128 + + + + +
diff --git a/integration-tests/spark-native/0003-filter-rows.hpl b/integration-tests/spark-native/0003-filter-rows.hpl new file mode 100644 index 00000000000..29a1e3e4524 --- /dev/null +++ b/integration-tests/spark-native/0003-filter-rows.hpl @@ -0,0 +1,193 @@ + + + + + 0003-filter-rows + Y + Native Spark Filter Rows: state STARTS WITH NO + Normal + + N + - + 2026/07/15 00:00:00.000 + - + 2026/07/15 00:00:00.000 + + + + + Spark File Input + State starts with NO + Y + + + State starts with NO + Spark File Output + Y + + + + SparkFileInput + Spark File Input + + Y + 1 + + none + + + ${PROJECT_HOME}/files/customers-1k.txt + csv +
Y
+ ; + " + N + N + + + + id + Integer + 7 + -1 + + + name + String + 50 + -1 + + + firstname + String + 50 + -1 + + + zip + String + 50 + -1 + + + city + String + 50 + -1 + + + birthdate + String + 20 + -1 + + + street + String + 50 + -1 + + + housenr + String + 50 + -1 + + + stateCode + String + 2 + -1 + + + state + String + 50 + -1 + + + + + 160 + 128 + +
+ + FilterRows + State starts with NO + + Y + 1 + + none + + + + + N + - + state + STARTS WITH + + constant + String + NO + -1 + -1 + N + + + + + + + + + + 368 + 128 + + + + SparkFileOutput + Spark File Output + + Y + 1 + + none + + + ${PROJECT_HOME}/output/0003-filter-rows + csv + Overwrite +
Y
+ , + " + + 1 + + + + 592 + 128 + +
+ + +
diff --git a/integration-tests/spark-native/0004-stream-lookup-validation.hpl b/integration-tests/spark-native/0004-stream-lookup-validation.hpl new file mode 100644 index 00000000000..9ddb7234bd5 --- /dev/null +++ b/integration-tests/spark-native/0004-stream-lookup-validation.hpl @@ -0,0 +1,304 @@ + + + + + 0004-stream-lookup-validation + Y + Read Stream Lookup output CSV on Local and expose Verify for unit test golden + Normal + + N + - + 2026/07/15 00:00:00.000 + - + 2026/07/15 00:00:00.000 + + + + + Read output CSV + Verify + Y + + + + Read output CSV + TextFileInput2 + + Y + + 1 + + none + + + N + N + + + , + " + N + +
Y
+ 1 +
N
+ 1 + N + 1 + N + 80 + 0 + Y + N + + N + N + + Unix + + Characters + Y + + ${PROJECT_HOME}/output/0004-stream-lookup/ + .*\.csv + + Y + N + CSV + None + + + + + id + Integer + # + + + + + + -1 + -1 + -1 + both + N + + + name + String + + + + + + + -1 + -1 + -1 + both + N + + + firstname + String + + + + + + + -1 + -1 + -1 + both + N + + + zip + String + + + + + + + -1 + -1 + -1 + both + N + + + city + String + + + + + + + -1 + -1 + -1 + both + N + + + birthdate + String + + + + + + + -1 + -1 + -1 + both + N + + + street + String + + + + + + + -1 + -1 + -1 + both + N + + + housenr + String + + + + + + + -1 + -1 + -1 + both + N + + + stateCode + String + + + + + + + -1 + -1 + -1 + both + N + + + state + String + + + + + + + -1 + -1 + -1 + both + N + + + countPerState + Integer + # + + + + + + -1 + -1 + 0 + both + N + + + 0 + N + N + + + N + + + + + warning + + error + + line + Y + + + + + + + + + + + + 176 + 128 + +
+ + Verify + Dummy + + Y + + 1 + + none + + + + + 432 + 128 + + + + +
diff --git a/integration-tests/spark-native/0004-stream-lookup.hpl b/integration-tests/spark-native/0004-stream-lookup.hpl new file mode 100644 index 00000000000..56b132a0b88 --- /dev/null +++ b/integration-tests/spark-native/0004-stream-lookup.hpl @@ -0,0 +1,230 @@ + + + + + 0004-stream-lookup + Y + Native Spark Stream Lookup: count per stateCode side-loaded via info stream + Normal + + N + - + 2026/07/15 00:00:00.000 + - + 2026/07/15 00:00:00.000 + + + + + Spark File Input + countPerState + Y + + + Spark File Input + Lookup count per state + Y + + + countPerState + Lookup count per state + Y + + + Lookup count per state + Spark File Output + Y + + + + SparkFileInput + Spark File Input + + Y + 1 + + none + + + ${PROJECT_HOME}/files/customers-1k.txt + csv +
Y
+ ; + " + N + N + + + + id + Integer + 7 + -1 + + + name + String + 50 + -1 + + + firstname + String + 50 + -1 + + + zip + String + 50 + -1 + + + city + String + 50 + -1 + + + birthdate + String + 20 + -1 + + + street + String + 50 + -1 + + + housenr + String + 50 + -1 + + + stateCode + String + 2 + -1 + + + state + String + 50 + -1 + + + + + 128 + 160 + +
+ + MemoryGroupBy + countPerState + + Y + 1 + + none + + + N + + + stateCode + + + + + countPerState + id + COUNT_ALL + + + + + + 336 + 64 + + + + StreamLookup + Lookup count per state + + Y + 1 + + none + + + countPerState + N + N + N + N + + + stateCode + stateCode + + + countPerState + countPerState + 0 + Integer + + + + + 512 + 160 + + + + SparkFileOutput + Spark File Output + + Y + 1 + + none + + + ${PROJECT_HOME}/output/0004-stream-lookup + csv + Overwrite +
Y
+ , + " + + 1 + + + + 704 + 160 + +
+ + +
diff --git a/integration-tests/spark-native/0005-filter-targets-validation.hpl b/integration-tests/spark-native/0005-filter-targets-validation.hpl new file mode 100644 index 00000000000..7b2503309ad --- /dev/null +++ b/integration-tests/spark-native/0005-filter-targets-validation.hpl @@ -0,0 +1,328 @@ + + + + + 0005-filter-targets-validation + Y + Validate dual filter-target CSV outputs + Normal + + N + - + 2026/07/15 00:00:00.000 + - + 2026/07/15 00:00:00.000 + + + + Read true CSVVerify trueY + Read false CSVVerify falseY + + + Read true CSV + TextFileInput2 + + Y + + 1 + + none + + + N + N + + + , + " + N + +
Y
+ 1 +
N
+ 1 + N + 1 + N + 80 + 0 + Y + N + + N + N + + Unix + + Characters + Y + + ${PROJECT_HOME}/output/0005-filter-true/ + .*\.csv + + Y + N + CSV + None + + + + + id + Integer + # + + + + + + -1 + -1 + -1 + both + N + + + stateCode + String + + + + + + + -1 + -1 + -1 + both + N + + + state + String + + + + + + + -1 + -1 + -1 + both + N + + + 0 + N + N + + + N + + + + + warning + + error + + line + Y + + + + + + + + + + + + + 128 + 96 + +
+ + + Verify true + Dummy + + Y + 1 + + none + + + + + 320 + 96 + + + + + Read false CSV + TextFileInput2 + + Y + + 1 + + none + + + N + N + + + , + " + N + +
Y
+ 1 +
N
+ 1 + N + 1 + N + 80 + 0 + Y + N + + N + N + + Unix + + Characters + Y + + ${PROJECT_HOME}/output/0005-filter-false/ + .*\.csv + + Y + N + CSV + None + + + + + id + Integer + # + + + + + + -1 + -1 + -1 + both + N + + + stateCode + String + + + + + + + -1 + -1 + -1 + both + N + + + state + String + + + + + + + -1 + -1 + -1 + both + N + + + 0 + N + N + + + N + + + + + warning + + error + + line + Y + + + + + + + + + + + + + 128 + 224 + +
+ + + Verify false + Dummy + + Y + 1 + + none + + + + + 320 + 224 + + + + + +
diff --git a/integration-tests/spark-native/0005-filter-targets.hpl b/integration-tests/spark-native/0005-filter-targets.hpl new file mode 100644 index 00000000000..f6edb9bfb40 --- /dev/null +++ b/integration-tests/spark-native/0005-filter-targets.hpl @@ -0,0 +1,171 @@ + + + + + 0005-filter-targets + Y + Native Spark Filter Rows with true/false target streams + Normal + + N + - + 2026/07/15 00:00:00.000 + - + 2026/07/15 00:00:00.000 + + + + Spark File InputState starts with NOY + State starts with NOTrue: selectY + State starts with NOFalse: selectY + True: selectoutput/trueY + False: selectoutput/falseY + + + SparkFileInput + Spark File Input + + Y + 1 + none + ${PROJECT_HOME}/files/customers-1k.txt + csv +
Y
+ ; + " + N + N + + + idInteger7-1 + nameString50-1 + firstnameString50-1 + zipString50-1 + cityString50-1 + birthdateString20-1 + streetString50-1 + housenrString50-1 + stateCodeString2-1 + stateString50-1 + + + 96160 +
+ + FilterRows + State starts with NO + + Y + 1 + none + + + N + - + state + STARTS WITH + + constant + String + NO + -1 + -1 + N + + + + + + True: select + False: select + + 288160 + + + SelectValues + True: select + + Y + 1 + none + + id-2-2 + stateCode-2-2 + state-2-2 + N + + + 48096 + + + SelectValues + False: select + + Y + 1 + none + + id-2-2 + stateCode-2-2 + state-2-2 + N + + + 480224 + + + SparkFileOutput + output/true + + Y + 1 + none + ${PROJECT_HOME}/output/0005-filter-true + csv + Overwrite +
Y
+ , + " + + 1 + + + 67296 +
+ + SparkFileOutput + output/false + + Y + 1 + none + ${PROJECT_HOME}/output/0005-filter-false + csv + Overwrite +
Y
+ , + " + + 1 + + + 672224 +
+ + +
diff --git a/integration-tests/spark-native/0006-switch-case-validation.hpl b/integration-tests/spark-native/0006-switch-case-validation.hpl new file mode 100644 index 00000000000..9b3b6ce5405 --- /dev/null +++ b/integration-tests/spark-native/0006-switch-case-validation.hpl @@ -0,0 +1,165 @@ + + + + + 0006-switch-case-validation + Y + Validate Switch/Case CA target CSV + Normal + + N + - + 2026/07/15 00:00:00.000 + - + 2026/07/15 00:00:00.000 + + + + Read CA CSVVerifyY + + + Read CA CSV + TextFileInput2 + + Y + + 1 + + none + + + N + N + + + , + " + N + +
Y
+ 1 +
N
+ 1 + N + 1 + N + 80 + 0 + Y + N + + N + N + + Unix + + Characters + Y + + ${PROJECT_HOME}/output/0006-switch-ca/ + .*\.csv + + Y + N + CSV + None + + + + + id + Integer + # + + + + + + -1 + -1 + -1 + both + N + + + stateCode + String + + + + + + + -1 + -1 + -1 + both + N + + + 0 + N + N + + + N + + + + + warning + + error + + line + Y + + + + + + + + + + + + + 128 + 96 + +
+ + Verify + Dummy + + Y + 1 + + none + + + + + 320 + 96 + + + + +
diff --git a/integration-tests/spark-native/0006-switch-case.hpl b/integration-tests/spark-native/0006-switch-case.hpl new file mode 100644 index 00000000000..9d7dc6e7238 --- /dev/null +++ b/integration-tests/spark-native/0006-switch-case.hpl @@ -0,0 +1,251 @@ + + + + + 0006-switch-case + Y + Native Spark Switch/Case target streams for stateCode CA/FL/NY/Default + Normal + + N + - + 2026/07/15 00:00:00.000 + - + 2026/07/15 00:00:00.000 + + + + Spark File InputSwitch / caseY + Switch / caseCAY + Switch / caseFLY + Switch / caseNYY + Switch / caseDefaultY + CAoutput/CAY + FLoutput/FLY + NYoutput/NYY + Defaultoutput/DefaultY + + + SparkFileInput + Spark File Input + + Y + 1 + none + ${PROJECT_HOME}/files/customers-1k.txt + csv +
Y
+ ; + " + N + N + + + idInteger7-1 + nameString50-1 + firstnameString50-1 + zipString50-1 + cityString50-1 + birthdateString20-1 + streetString50-1 + housenrString50-1 + stateCodeString2-1 + stateString50-1 + + + + 96192 +
+ + SwitchCase + Switch / case + + Y + 1 + none + stateCode + N + String + + + + Default + + + CA + CA + + + FL + FL + + + NY + NY + + + + 288192 + + + SelectValues + CA + + Y + 1 + none + + id-2-2 + stateCode-2-2 + N + + + 48064 + + + + SelectValues + FL + + Y + 1 + none + + id-2-2 + stateCode-2-2 + N + + + 480144 + + + + SelectValues + NY + + Y + 1 + none + + id-2-2 + stateCode-2-2 + N + + + 480224 + + + + SelectValues + Default + + Y + 1 + none + + id-2-2 + stateCode-2-2 + N + + + 480304 + + + + SparkFileOutput + output/CA + + Y + 1 + none + ${PROJECT_HOME}/output/0006-switch-ca + csv + Overwrite +
Y
+ , + " + + 1 + + + 67264 +
+ + + SparkFileOutput + output/FL + + Y + 1 + none + ${PROJECT_HOME}/output/0006-switch-fl + csv + Overwrite +
Y
+ , + " + + 1 + + + 672144 +
+ + + SparkFileOutput + output/NY + + Y + 1 + none + ${PROJECT_HOME}/output/0006-switch-ny + csv + Overwrite +
Y
+ , + " + + 1 + + + 672224 +
+ + + SparkFileOutput + output/Default + + Y + 1 + none + ${PROJECT_HOME}/output/0006-switch-default + csv + Overwrite +
Y
+ , + " + + 1 + + + 672304 +
+ + + +
diff --git a/integration-tests/spark-native/0007-text-file-input-create-test-files.hpl b/integration-tests/spark-native/0007-text-file-input-create-test-files.hpl new file mode 100644 index 00000000000..e21c069833c --- /dev/null +++ b/integration-tests/spark-native/0007-text-file-input-create-test-files.hpl @@ -0,0 +1,314 @@ + + + + + N + 1000 + 100 + Normal + -1 + + New pipeline + Y + - + - + 2026/07/16 11:49:46.901 + 2026/07/16 11:49:46.901 + + + RowGenerator + 100 rows + N + 5000 + now + FiveSecondsAgo + 100 + + + prefix + String + + -1 + -1 + + + + test + N + + + Y + 1 + + 128 + 128 + + + none + + + + + + Sequence + id + id + N + + + SEQ_ + Y + + 1 + 1 + 999999999 + Y + 1 + + 240 + 128 + + + none + + + + + + ConcatFields + test + - + + N + + + prefix + String + + -1 + -1 + + + + + + + + id + String + + 3 + 0 + + + + + + + + + test + 100 + N + + N + Y + 1 + + 464 + 128 + + + none + + + + + + SelectValues + format id + + N + + id + id + String + -2 + 0 + + 000 + N + + + N + + + + + half_even + + + Y + 1 + + 352 + 128 + + + none + + + + + + SelectValues + id, test + + + id + + -2 + -2 + + + test + + -2 + -2 + + N + + Y + 1 + + 560 + 128 + + + none + + + + + + TextFileOutput + output/0007-text-file-input/source/test-file.*.csv + None + Y + , + " + N + N +
Y
+
N
+ UNIX + UTF-8 + + N + + + N + + ${PROJECT_HOME}/output/0007-text-file-input/source/test-file + Y + csv + N + Y + N + N + N + N + + N + N + N + + + + + id + String + + -1 + -1 + + + + + both + half_even + + + test + String + + -1 + -1 + + + + + both + half_even + + + Y + 3 + + 784 + 128 + + + none + + + +
+ + + 100 rows + id + Y + + + id + format id + Y + + + format id + test + Y + + + test + id, test + Y + + + id, test + output/0007-text-file-input/source/test-file.*.csv + Y + + + + + +
diff --git a/integration-tests/spark-native/0007-text-file-input-spark-readback.hpl b/integration-tests/spark-native/0007-text-file-input-spark-readback.hpl new file mode 100644 index 00000000000..717282a53c5 --- /dev/null +++ b/integration-tests/spark-native/0007-text-file-input-spark-readback.hpl @@ -0,0 +1,226 @@ + + + + + N + 1000 + 100 + Normal + -1 + + New pipeline + Y + - + - + 2026/07/16 12:03:19.564 + 2026/07/16 12:03:19.564 + + + GetFileNames + source/test-file*.csv + + ${PROJECT_HOME}/output/0007-text-file-input/source/ + test-file.*.csv + + N + N + + + all_files + + N + + + + + N + N + Y + 0 + N + Y + Y + 1 + + 112 + 96 + + + none + + + + + + SparkFileOutput + parquet/*.parquet + ${PROJECT_HOME}/output/0007-text-file-input/parquet + parquet + Overwrite +
Y
+ , + " + + + + Y + 1 + + 400 + 96 + + + none + + + +
+ + TextFileInput2 + read test-file + CSV + None + , + " + N + \ +
Y
+ N + 1 +
N
+ 1 + N + 1 + N + 80 + 0 + Y + N + + N + + N + Unix + + 0 + Y + en_US + + + + + + N + + N + + + + + + + + + + + ${PROJECT_HOME}/output/0007-text-file-input/source/test-file_0.csv + + + N + N + + + Y + source/test-file*.csv + N + filename + Y + N + + + N + + warning + + error + + line + + + id + -1 + 15 + Integer + N + # + none + 0 + $ + . + , + N + - + + + + test + -1 + 8 + String + N + + none + -1 + $ + . + , + N + - + + + + Y + 1 + + 256 + 96 + + + none + + + +
+ + + source/test-file*.csv + read test-file + Y + + + read test-file + parquet/*.parquet + Y + + + + + +
diff --git a/integration-tests/spark-native/0007-text-file-input-validate.hpl b/integration-tests/spark-native/0007-text-file-input-validate.hpl new file mode 100644 index 00000000000..5a54c50f970 --- /dev/null +++ b/integration-tests/spark-native/0007-text-file-input-validate.hpl @@ -0,0 +1,307 @@ + + + + + N + 1000 + 100 + Normal + -1 + + New pipeline + Y + - + - + 2026/07/16 13:42:59.911 + 2026/07/16 13:42:59.911 + + + ParquetFileInput + *.parquet + filename + + N + + + id + id + Integer + + + + + + test + test + String + + + + + + Y + 1 + + 304 + 112 + + + none + + + + + + GetFileNames + Get file names + + ${PROJECT_HOME}/output/0007-text-file-input/parquet/ + .*.parquet + + N + N + + + all_files + + N + + + + + N + N + Y + 0 + N + Y + Y + 1 + + 144 + 112 + + + none + + + + + + SelectValues + id, test + + + id + + -2 + -2 + + + test + + -2 + -2 + + N + + Y + 1 + + 416 + 112 + + + none + + + + + + MemoryGroupBy + counts + + + + countDistinctId + id + COUNT_DISTINCT + + + + countDistinctTest + test + COUNT_DISTINCT + + + + N + Y + 1 + + 528 + 112 + + + none + + + + + + Validator + Data validator + + unique-id-100 + countDistinctId + + + Y + N + N + Integer + Y + 0 + + + N + + + + + + + + + + + + + + 100 + + + + unique-test-100 + countDistinctTest + + + Y + N + N + Integer + Y + 0 + + + N + + + + + + + + + + + + + + 100 + + + N + N + + Y + 1 + + 656 + 112 + + + none + + + + + + Abort + Abort + 0 + We didn't read back exactly 100 rows + Y + ABORT_WITH_ERROR + Y + 1 + + 784 + 112 + + + none + + + + + + + Get file names + *.parquet + Y + + + *.parquet + id, test + Y + + + id, test + counts + Y + + + counts + Data validator + Y + + + Data validator + Abort + Y + + + + + + + Data validator + Abort + Y + error_row + error_description + error_code + + + + + + diff --git a/integration-tests/spark-native/0008-excel-stream-lookup-create-test-files.hpl b/integration-tests/spark-native/0008-excel-stream-lookup-create-test-files.hpl new file mode 100644 index 00000000000..e63a2225d6f --- /dev/null +++ b/integration-tests/spark-native/0008-excel-stream-lookup-create-test-files.hpl @@ -0,0 +1,317 @@ + + + + + 0008-excel-stream-lookup-create-test-files + Y + Local: build products.xlsx (lookup) and 3 order parquet part files (main stream) + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + product catalog + products.xlsx + Y + + + orders + orders parquet (3 parts) + Y + + + + + + DataGrid + product catalog + Lookup dimension: productCode → productName, unitPrice + Y + 1 + + none + + + + + AAA + Alpha Widget + 10 + + + BBB + Beta Gadget + 20 + + + CCC + Gamma Device + 30 + + + + + productCode + String + -1 + -1 + N + + + productName + String + -1 + -1 + N + + + unitPrice + Integer + -1 + 0 + N + + + + + 128 + 96 + + + + TypeExitExcelWriterTransform + products.xlsx + + Y + 1 + + none + + + N + 0 + N + 0 + N + + + productCode + String + productCode + N + + + + + + + + + productName + String + productName + N + + + + + + + + + unitPrice + Integer + unitPrice + N + + + + + + + + + + ${PROJECT_HOME}/output/0008-excel-stream-lookup/source/products + xlsx + Sheet1 + Y + Y + N + 0 + N + N + N + + N + + new + new + N + + + N + N + +
Y
+
N
+ Y + overwrite + A1 + N + N + + + + 352 + 96 + +
+ + + + DataGrid + orders + Main fact rows: 6 orders → ParquetFileOutput splits every 2 rows → 3 files + Y + 1 + + none + + + + + 1 + AAA + 2 + + + 2 + BBB + 1 + + + 3 + CCC + 5 + + + 4 + AAA + 1 + + + 5 + BBB + 3 + + + 6 + CCC + 2 + + + + + orderId + Integer + -1 + 0 + N + + + productCode + String + -1 + -1 + N + + + qty + Integer + -1 + 0 + N + + + + + 128 + 256 + + + + ParquetFileOutput + orders parquet (3 parts) + split_size=2 → three *.parquet part files under source/ + Y + 1 + + none + + + UNCOMPRESSED + 8192 + 1048576 + + + orderId + orderId + + + productCode + productCode + + + qty + qty + + + ${PROJECT_HOME}/output/0008-excel-stream-lookup/source/parquet/orders + Y + yyyyMMdd-HHmmss + parquet + N + N + N + Y + N + 2 + 20000 + 1.0 + + + 352 + 256 + + + + +
diff --git a/integration-tests/spark-native/0008-excel-stream-lookup-validation.hpl b/integration-tests/spark-native/0008-excel-stream-lookup-validation.hpl new file mode 100644 index 00000000000..ebc1de08ca1 --- /dev/null +++ b/integration-tests/spark-native/0008-excel-stream-lookup-validation.hpl @@ -0,0 +1,265 @@ + + + + + N + 1000 + 100 + Normal + 0 + + 0008-excel-stream-lookup-validation + Y + Local: read Spark CSV result and expose Verify for golden unit test + - + - + 2026/07/16 00:00:00.000 + 2026/07/16 00:00:00.000 + + + TextFileInput2 + Read result CSV + CSV + None + , + " + N + +
Y
+ N + 1 +
N
+ 1 + N + 1 + N + 80 + 0 + Y + N + + N + + N + Unix + UTF-8 + -1 + Y + en_US + Characters + + + + + N + + N + + + + + + + + + + + ${PROJECT_HOME}/output/0008-excel-stream-lookup/result/ + .*\.csv + + Y + N + + + N + + N + + N + N + + + N + + + + + + + + + orderId + -1 + -1 + Integer + N + # + both + 0 + + + + N + + + + + productCode + -1 + -1 + String + N + + both + -1 + + + + N + + + + + qty + -1 + -1 + Integer + N + # + both + 0 + + + + N + + + + + productName + -1 + -1 + String + N + + both + -1 + + + + N + + + + + unitPrice + -1 + -1 + Integer + N + # + both + 0 + + + + N + + + + + Y + 1 + + 128 + 128 + + + + none + + + +
+ + SortRows + Sort by orderId + + + orderId + Y + N + N + 0 + N + + + ${java.io.tmpdir} + out + 1000000 + + N + N + + Y + 1 + + 336 + 128 + + Stable order for golden compare (parquet parts are unordered) + + none + + + + + + Dummy + Verify + Y + 1 + + 544 + 128 + + Unit-test golden attachment point + + none + + + + + + + Read result CSV + Sort by orderId + Y + + + Sort by orderId + Verify + Y + + + + + +
diff --git a/integration-tests/spark-native/0008-excel-stream-lookup.hpl b/integration-tests/spark-native/0008-excel-stream-lookup.hpl new file mode 100644 index 00000000000..3a33b88b72e --- /dev/null +++ b/integration-tests/spark-native/0008-excel-stream-lookup.hpl @@ -0,0 +1,262 @@ + + + + + 0008-excel-stream-lookup + Y + Native Spark: main from 3 Parquet files + Excel info stream into Stream Lookup + Normal + + N + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + + orders parquet/* + Lookup product + Y + + + products.xlsx + Lookup product + Y + + + Lookup product + result csv + Y + + + + + SparkFileInput + orders parquet/* + Main stream: all orders-*.parquet under source/ (3 part files) + Y + 1 + + none + + + ${PROJECT_HOME}/output/0008-excel-stream-lookup/source/parquet + parquet +
Y
+ , + " + N + N + + + + orderId + Integer + -1 + 0 + + + productCode + String + -1 + -1 + + + qty + Integer + -1 + 0 + + + + + 128 + 192 + +
+ + + ExcelInput + products.xlsx + Info stream for Stream Lookup (classic Excel on mapPartitions) + Y + 1 + + none + + +
Y
+ Y + N + + + + + 0 + + N + N + + + + ${PROJECT_HOME}/output/0008-excel-stream-lookup/source/products.xlsx + + + Y + N + + + + productCode + String + -1 + -1 + both + N + + + + + + + productName + String + -1 + -1 + both + N + + + + + + + unitPrice + Integer + -1 + 0 + both + N + + + + + + + + + Sheet1 + 0 + 0 + + + N + N + N + + warning + + error + + line + + + + + + + + + POI + + + 128 + 64 + +
+ + + StreamLookup + Lookup product + Main = parquet orders; info = Excel product catalog + Y + 1 + + none + + + products.xlsx + N + N + N + N + + + productCode + productCode + + + productName + productName + + String + + + unitPrice + unitPrice + 0 + Integer + + + + + 384 + 192 + + + + + SparkFileOutput + result csv + + Y + 1 + + none + + + ${PROJECT_HOME}/output/0008-excel-stream-lookup/result + csv + Overwrite +
Y
+ , + " + + 1 + + + + 608 + 192 + +
+ + +
diff --git a/integration-tests/spark-native/0099-complex.hpl b/integration-tests/spark-native/0099-complex.hpl new file mode 100644 index 00000000000..812e8038f34 --- /dev/null +++ b/integration-tests/spark-native/0099-complex.hpl @@ -0,0 +1,662 @@ + + + + + N + 1000 + 100 + Normal + -1 + + New pipeline + Y + - + - + 2026/07/15 23:15:37.182 + 2026/07/15 23:15:37.182 + + + Constant + CA + + + + + + Some comment about California + Comment + String + + -1 + -1 + N + + + Y + 1 + + 912 + 96 + + + none + + + + + + Dummy + Collect + Y + 1 + + 1024 + 192 + + + none + + + + + + Constant + Default + + + + + + no comment. + Comment + String + + -1 + -1 + N + + + Y + 1 + + 912 + 288 + + + none + + + + + + Constant + FL + + + + + + Some remark on Floridians + Comment + String + + -1 + -1 + N + + + Y + 1 + + 912 + 160 + + + none + + + + + + Constant + Label: A-M + + + + + + A-M + label + String + + -1 + -1 + N + + + Y + 1 + + 688 + 96 + + + none + + + + + + Constant + Label: N-Z + + + + + + N-Z + label + String + + -1 + -1 + N + + + Y + 1 + + 688 + 288 + + + none + + + + + + StreamLookup + Lookup count per state + countPerState + N + N + N + Y + + + stateCode + stateCode + + + countPerState + nrPerState + + Integer + + + Y + 1 + + 496 + 192 + + + none + + + + + + MergeJoin + Merge join + LEFT OUTER + files/customers-noheader-1k.txt + uppercase state + + state + + + state + + Y + 1 + + 384 + 192 + + + none + + + + + + Constant + NY + + + + + + New York rocks! + Comment + String + + -1 + -1 + N + + + Y + 1 + + 912 + 224 + + + none + + + + + + SwitchCase + Switch / case + stateCode + String + + + + + + CA + CA + + + FL + FL + + + NY + NY + + + Default + N + Y + 1 + + 768 + 192 + + + none + + + + + + MemoryGroupBy + countPerState + + + stateCode + + + + + countPerState + id + COUNT_ALL + + + + N + Y + 1 + + 384 + 80 + + + none + + + + + + FilterRows + name<n + + + N + - + name + < + + + constant + String + n + -1 + -1 + N + + + + + + Label: A-M + Label: N-Z + Y + 1 + + 608 + 192 + + + none + + + + + + StringOperations + uppercase state + + + state + + none + upper + no + none + none + none + none + + + + + Y + 1 + + 384 + 288 + + + none + + + + + + SparkFileInput + files/customers-noheader-1k.txt + ${PROJECT_HOME}/files/customers-noheader-1k.txt + csv +
N
+ ; + " + N + N + + + + id + Integer + 7 + -1 + + + name + String + 50 + -1 + + + firstname + String + 50 + -1 + + + zip + String + 50 + -1 + + + city + String + 50 + -1 + + + birthdate + String + 20 + -1 + + + street + String + 50 + -1 + + + housenr + String + 50 + -1 + + + stateCode + String + 2 + -1 + + + state + String + 50 + -1 + + + N + 1 + + 224 + 192 + + + none + + + +
+ + SparkFileInput + files/state-population.txt + ${PROJECT_HOME}/files/state-population.txt + csv +
N
+ ; + " + N + N + + + + state + String + 100 + -1 + + + count + Integer + 8 + -1 + + + Y + 1 + + 224 + 288 + + + none + + + +
+ + SparkFileOutput + output/0099-complex.csv + ${PROJECT_HOME}/output/0099-complex + csv + Overwrite +
Y
+ , + " + + + + Y + 1 + + 1152 + 192 + + + none + + + +
+ + + CA + Collect + Y + + + Default + Collect + Y + + + FL + Collect + Y + + + Label: A-M + Switch / case + Y + + + Label: N-Z + Switch / case + Y + + + Lookup count per state + name<n + Y + + + Merge join + Lookup count per state + Y + + + NY + Collect + Y + + + Switch / case + CA + Y + + + Switch / case + Default + Y + + + Switch / case + FL + Y + + + Switch / case + NY + Y + + + countPerState + Lookup count per state + Y + + + name<n + Label: A-M + Y + + + name<n + Label: N-Z + Y + + + uppercase state + Merge join + Y + + + files/customers-noheader-1k.txt + countPerState + Y + + + files/state-population.txt + uppercase state + Y + + + Collect + output/0099-complex.csv + Y + + + files/customers-noheader-1k.txt + Merge join + Y + + + + + +
diff --git a/integration-tests/spark-native/datasets/0001-customers-1k-golden.csv b/integration-tests/spark-native/datasets/0001-customers-1k-golden.csv new file mode 100644 index 00000000000..89b2d044c7f --- /dev/null +++ b/integration-tests/spark-native/datasets/0001-customers-1k-golden.csv @@ -0,0 +1,1001 @@ +id,name,firstname,zip,city,birthdate,street,housenr,stateCode,state +1,jwcdf-name,fsj-firstname,13520,oem-city,1954/02/07,amrb-street,145,AK,ALASKA +2,flhxu-name,tum-firstname,17520,buo-city,1966/04/24,wfyz-street,96,GA,GEORGIA +3,xthfg-name,gfe-firstname,12560,vtz-city,1990/01/11,doxx-street,46,NJ,NEW JERSEY +4,ulzrz-name,bnl-firstname,11620,prz-city,1966/08/02,bxqn-street,104,NY,NEW YORK +5,oxhyr-name,onx-firstname,15180,bpn-city,1970/11/14,pksn-street,133,IN,INDIANA +6,fiqjz-name,sce-firstname,16020,fnn-city,1954/09/24,wbhg-street,35,MD,MARYLAND +7,tkiat-name,xti-firstname,12720,stt-city,1966/08/11,tvnf-street,21,PA,PENNSYLVANIA +8,kljcz-name,uqd-firstname,13340,ntt-city,1987/01/15,jyje-street,10,PW,PALAU +9,pgunz-name,hcm-firstname,16680,gxh-city,1970/11/08,shbe-street,184,NC,NORTH CAROLINA +10,oyjha-name,uhj-firstname,18880,uyg-city,1966/04/10,bjgw-street,176,AR,ARKANSAS +11,igxbd-name,uph-firstname,13480,ndh-city,1962/12/03,jdcd-street,151,NH,NEW HAMPSHIRE +12,vnaov-name,wha-firstname,13120,egm-city,1954/03/28,hpep-street,20,CA,CALIFORNIA +13,dauuz-name,hwg-firstname,13740,khn-city,1958/05/15,etqx-street,5,OK,OKLAHOMA +14,gkuuo-name,kkb-firstname,13560,xdt-city,1962/04/07,sdoj-street,35,MT,MONTANA +15,wdhze-name,jjk-firstname,16900,due-city,1970/07/17,pmmu-street,174,AS,AMERICAN SAMOA +16,ncayz-name,ynb-firstname,15720,lxj-city,1974/04/27,mdtb-street,109,MA,MASSACHUSETTS +17,rdjin-name,hhu-firstname,14480,lpc-city,1958/11/16,wxik-street,145,KY,KENTUCKY +18,nxzij-name,bdl-firstname,10740,avx-city,1958/02/20,nybz-street,138,WI,WISCONSIN +19,xgrzc-name,dxw-firstname,18900,vpq-city,1990/11/16,wzjh-street,58,ME,MAINE +20,ehgrn-name,vbe-firstname,17500,cik-city,1978/05/21,ucnw-street,135,MD,MARYLAND +21,gctjx-name,upx-firstname,11960,yqr-city,1958/03/03,rlko-street,141,TN,TENNESSEE +22,ptzmg-name,hva-firstname,15740,gux-city,1978/05/04,pugy-street,122,VI,VIRGIN ISLANDS +23,eyeti-name,gnw-firstname,17420,eko-city,1962/10/26,ylph-street,61,NC,NORTH CAROLINA +24,wccwo-name,zpj-firstname,16600,uim-city,1962/09/29,ygih-street,26,WA,WASHINGTON +25,bwkoe-name,ayl-firstname,18660,rtw-city,1978/07/16,mzww-street,179,CA,CALIFORNIA +26,rezku-name,zio-firstname,19080,nvt-city,1982/07/14,wwkd-street,91,CA,CALIFORNIA +27,mjlsk-name,ecx-firstname,10800,yxu-city,1950/12/11,vttb-street,195,MO,MISSOURI +28,wdjsi-name,aoq-firstname,13660,smo-city,1954/02/01,kako-street,7,NV,NEVADA +29,mwfnd-name,nyb-firstname,19760,bbu-city,1986/09/23,apdi-street,91,MS,MISSISSIPPI +30,vtuoz-name,jhh-firstname,17620,vad-city,1982/05/05,kzup-street,79,GA,GEORGIA +31,rhhxk-name,ndr-firstname,16760,fub-city,1978/11/12,regd-street,55,OK,OKLAHOMA +32,lpstk-name,mqz-firstname,18940,tnr-city,1982/09/16,cdhf-street,4,SD,SOUTH DAKOTA +33,ldhyr-name,yts-firstname,12000,auk-city,1986/11/14,abph-street,147,IN,INDIANA +34,cjdml-name,iti-firstname,16900,wkq-city,1970/06/05,npow-street,96,NH,NEW HAMPSHIRE +35,cpenz-name,sbi-firstname,16380,ssl-city,1962/08/19,kilz-street,44,MS,MISSISSIPPI +36,rxtbg-name,anr-firstname,14720,bqc-city,1958/08/10,pudg-street,140,NV,NEVADA +37,udblf-name,raa-firstname,11500,wli-city,1978/12/13,xomd-street,41,PW,PALAU +38,vvyce-name,gep-firstname,13740,gtd-city,1982/05/23,kwbv-street,123,undefined,undefined +39,kwfnz-name,ucu-firstname,10580,sns-city,1978/08/18,nnun-street,20,OK,OKLAHOMA +40,zxydx-name,tml-firstname,14680,jda-city,1974/05/29,wfjn-street,157,DC,DISTRICT OF COLUMBIA +41,bfscx-name,jnl-firstname,16920,yyg-city,1970/11/30,cgfh-street,178,CO,COLORADO +42,qitur-name,yra-firstname,15560,ijp-city,1978/01/30,fonc-street,155,AK,ALASKA +43,msixi-name,ynb-firstname,12720,ksl-city,1958/07/17,zpjw-street,46,VI,VIRGIN ISLANDS +44,wzkjq-name,rgh-firstname,19000,hkm-city,1974/08/12,yixf-street,134,CA,CALIFORNIA +45,dqfmf-name,yxr-firstname,13840,vie-city,1962/10/23,stvx-street,39,TX,TEXAS +46,biluz-name,uqe-firstname,17760,wkq-city,1962/07/27,embn-street,183,PW,PALAU +47,wahfx-name,zwd-firstname,13240,vic-city,1974/03/27,axpw-street,131,UT,UTAH +48,denwt-name,bta-firstname,17300,hhj-city,1986/12/20,orwy-street,11,WV,WEST VIRGINIA +49,akdmy-name,ybz-firstname,14560,wtx-city,1962/11/08,nwba-street,123,MP,NORTHERN MARIANA ISLANDS +50,hqafg-name,nht-firstname,16080,gfu-city,1951/01/12,spsq-street,45,LA,LOUISIANA +51,zhmbl-name,lnw-firstname,17460,hse-city,1986/12/21,scis-street,97,GA,GEORGIA +52,snwnj-name,jyy-firstname,16400,hsz-city,1966/02/15,imhl-street,42,NC,NORTH CAROLINA +53,fuyla-name,mmp-firstname,11840,hgu-city,1986/08/16,ixiz-street,145,NC,NORTH CAROLINA +54,yvfqz-name,prz-firstname,11260,wjl-city,1982/05/06,fbzd-street,97,MO,MISSOURI +55,usbgq-name,vhd-firstname,14080,dsb-city,1958/04/01,ggoc-street,54,KS,KANSAS +56,yaeni-name,zpy-firstname,19100,sen-city,1954/12/10,sbsw-street,158,HI,HAWAII +57,fgxvr-name,vzi-firstname,17520,lcf-city,1958/11/01,nbdv-street,10,GU,GUAM +58,tqpbq-name,rwr-firstname,19140,zpd-city,1978/08/23,npvb-street,190,DC,DISTRICT OF COLUMBIA +59,ieigg-name,ayq-firstname,12960,ljc-city,1962/07/05,dnjz-street,163,FL,FLORIDA +60,rfvzu-name,edm-firstname,13340,kvz-city,1954/12/08,eijd-street,4,RI,RHODE ISLAND +61,pduwm-name,gqb-firstname,14240,cyr-city,1954/07/03,ndux-street,13,SD,SOUTH DAKOTA +62,yyixf-name,yzt-firstname,18020,lwx-city,1974/01/29,iede-street,120,NV,NEVADA +63,dkszq-name,ytd-firstname,14700,zwh-city,1979/01/11,nbjz-street,65,AS,AMERICAN SAMOA +64,slkzv-name,zbg-firstname,19880,oee-city,1978/11/01,sphg-street,119,OK,OKLAHOMA +65,nvxim-name,phc-firstname,19220,vgg-city,1991/01/24,juok-street,106,FM,FEDERATED STATES OF MICRONESIA +66,piyfg-name,xtn-firstname,13760,nde-city,1954/07/22,vfrv-street,11,NY,NEW YORK +67,jnusz-name,mjw-firstname,12640,nwb-city,1986/08/23,kcsa-street,138,VA,VIRGINIA +68,jnypj-name,ioq-firstname,17000,zqy-city,1986/01/09,croe-street,119,PW,PALAU +69,uohts-name,btx-firstname,13480,dal-city,1990/10/22,llyw-street,150,WA,WASHINGTON +70,aavpj-name,pvw-firstname,13780,lai-city,1954/09/23,nygu-street,171,FL,FLORIDA +71,nbjcj-name,rsf-firstname,12000,kjl-city,1986/06/30,ijsb-street,123,ID,IDAHO +72,syjxh-name,gkq-firstname,19960,rmd-city,1978/10/26,qmyp-street,161,MN,MINNESOTA +73,vkojz-name,ryo-firstname,14300,bmz-city,1954/09/11,gcpj-street,71,ND,NORTH DAKOTA +74,pqzfw-name,kld-firstname,16400,qvq-city,1962/09/09,dhbv-street,92,ND,NORTH DAKOTA +75,owvjk-name,fez-firstname,19740,ldb-city,1978/06/14,kabf-street,87,VA,VIRGINIA +76,qsfih-name,ixe-firstname,16860,qvr-city,1987/01/07,qean-street,159,CO,COLORADO +77,slixq-name,gmb-firstname,19980,ftt-city,1982/06/22,xinx-street,111,VT,VERMONT +78,eegsa-name,xlc-firstname,12680,byk-city,1954/04/23,beul-street,56,MD,MARYLAND +79,phevp-name,ihs-firstname,16120,adc-city,1978/04/25,voig-street,98,NM,NEW MEXICO +80,njfoe-name,tag-firstname,16580,tnr-city,1966/12/04,dhky-street,108,LA,LOUISIANA +81,bdncx-name,hcd-firstname,11260,xcl-city,1970/07/02,jvlp-street,49,GA,GEORGIA +82,ikedo-name,tks-firstname,17460,odl-city,1958/08/25,iaaq-street,8,GU,GUAM +83,iafxy-name,vur-firstname,11480,hgt-city,1962/08/03,hmec-street,164,TX,TEXAS +84,lafhf-name,ssz-firstname,19560,wwp-city,1951/01/25,mxmq-street,96,IN,INDIANA +85,okyny-name,hbu-firstname,16800,yok-city,1978/03/28,ipjz-street,135,NV,NEVADA +86,hznby-name,fwy-firstname,13680,wbi-city,1970/07/25,mxui-street,170,CT,CONNECTICUT +87,ztpoa-name,rzk-firstname,18500,qum-city,1970/07/26,blqr-street,152,ME,MAINE +88,gitxz-name,axt-firstname,11800,fck-city,1974/01/12,tmjw-street,189,SD,SOUTH DAKOTA +89,ziomm-name,mcv-firstname,12940,iwq-city,1950/10/22,hqgj-street,140,DC,DISTRICT OF COLUMBIA +90,otncg-name,tuy-firstname,16540,ulk-city,1971/01/24,yuia-street,166,TX,TEXAS +91,cnabb-name,hoq-firstname,16300,tuw-city,1962/06/17,ujvv-street,61,ME,MAINE +92,ucogf-name,ggc-firstname,14500,fsj-city,1978/02/08,asfi-street,53,WV,WEST VIRGINIA +93,lbpmf-name,sdt-firstname,10780,ewj-city,1978/03/08,hxsp-street,102,NV,NEVADA +94,tieqq-name,uyu-firstname,17740,wea-city,1966/10/31,abpl-street,187,MO,MISSOURI +95,fsgwf-name,vjd-firstname,12460,ads-city,1970/11/29,yeou-street,10,MA,MASSACHUSETTS +96,reeba-name,kzs-firstname,13100,zhc-city,1966/07/08,abmv-street,88,FL,FLORIDA +97,shybc-name,gcp-firstname,10660,ahg-city,1950/12/15,hrqy-street,174,KS,KANSAS +98,phszr-name,sst-firstname,13080,ydd-city,1954/09/23,quqn-street,2,RI,RHODE ISLAND +99,jteco-name,fxc-firstname,19760,agr-city,1986/05/06,dzxc-street,108,MD,MARYLAND +100,qvaar-name,icx-firstname,16120,boc-city,1978/08/04,bfzf-street,12,NM,NEW MEXICO +101,iiapu-name,veo-firstname,10180,wdv-city,1954/05/29,ovyu-street,55,WI,WISCONSIN +102,qfawh-name,wlx-firstname,11280,fad-city,1962/04/28,hibx-street,188,MP,NORTHERN MARIANA ISLANDS +103,nfurq-name,rib-firstname,17080,xcp-city,1962/11/10,rqui-street,164,MI,MICHIGAN +104,hdbiw-name,wxm-firstname,12600,txy-city,1978/11/23,yfcx-street,112,OK,OKLAHOMA +105,oyher-name,jws-firstname,17900,bai-city,1978/12/30,tyil-street,178,NC,NORTH CAROLINA +106,fzjnb-name,wxk-firstname,12300,fda-city,1974/03/26,aweg-street,7,MO,MISSOURI +107,ycpcp-name,xfq-firstname,12660,mna-city,1986/02/14,dcki-street,5,AZ,ARIZONA +108,rxxeb-name,qdw-firstname,17600,yks-city,1970/11/15,zvsf-street,74,MH,MARSHALL ISLANDS +109,ffbgl-name,fqf-firstname,11680,npo-city,1974/12/07,vvan-street,175,GA,GEORGIA +110,foygy-name,vog-firstname,16920,mun-city,1970/07/03,urct-street,153,HI,HAWAII +111,imoqe-name,xfe-firstname,14620,gfv-city,1986/02/20,nlak-street,181,AK,ALASKA +112,mnhwk-name,bbt-firstname,16180,bnf-city,1986/10/10,yybd-street,144,AL,ALABAMA +113,lkfyf-name,xhg-firstname,13260,myb-city,1958/10/27,jjcn-street,128,GA,GEORGIA +114,bkhya-name,hrh-firstname,11500,byw-city,1990/03/03,oubr-street,41,MH,MARSHALL ISLANDS +115,avceb-name,ztu-firstname,10020,ogo-city,1974/05/26,iuob-street,31,KS,KANSAS +116,pnacg-name,iws-firstname,11640,dtx-city,1966/06/01,kwwj-street,177,WV,WEST VIRGINIA +117,ypnsc-name,tyv-firstname,16820,glg-city,1962/12/19,nysz-street,13,OK,OKLAHOMA +118,agndn-name,qae-firstname,15540,tpg-city,1990/05/15,ygzx-street,166,NV,NEVADA +119,qqhon-name,tlb-firstname,19880,iif-city,1982/02/05,vsbj-street,191,MP,NORTHERN MARIANA ISLANDS +120,qzxic-name,mot-firstname,14840,qvf-city,1970/03/19,gelo-street,149,WA,WASHINGTON +121,lunca-name,ced-firstname,13700,wjp-city,1979/01/06,racn-street,54,ID,IDAHO +122,jsxhg-name,yoo-firstname,13440,ulf-city,1982/06/28,krry-street,55,NY,NEW YORK +123,ugbci-name,vht-firstname,17100,ffc-city,1962/10/16,ixha-street,99,VT,VERMONT +124,hgtkz-name,xgg-firstname,17500,qck-city,1978/07/17,xkez-street,60,AK,ALASKA +125,ejdoc-name,ovv-firstname,14920,ocs-city,1954/11/15,bwhd-street,169,FL,FLORIDA +126,zoucr-name,ivo-firstname,14040,grt-city,1990/04/29,qhox-street,90,WI,WISCONSIN +127,asofx-name,yzv-firstname,19600,ixo-city,1982/07/09,slmy-street,198,MO,MISSOURI +128,tgowi-name,zvm-firstname,17560,avv-city,1986/11/04,qnyp-street,83,AS,AMERICAN SAMOA +129,toeur-name,ydp-firstname,17260,dpz-city,1962/11/02,atag-street,60,IN,INDIANA +130,eohex-name,vfp-firstname,17000,gzc-city,1970/01/05,cvlk-street,188,UT,UTAH +131,mahci-name,cwt-firstname,18240,wut-city,1982/08/02,zyse-street,147,OR,OREGON +132,hzetq-name,kif-firstname,14280,aep-city,1990/11/26,rrsr-street,118,NE,NEBRASKA +133,tjrgp-name,vle-firstname,15620,sdv-city,1962/12/08,zdyx-street,88,WV,WEST VIRGINIA +134,japyi-name,jkm-firstname,14960,jeo-city,1958/03/01,bsxv-street,86,TX,TEXAS +135,eqley-name,ttv-firstname,12740,trs-city,1974/09/16,ibff-street,187,CA,CALIFORNIA +136,dbvkz-name,efr-firstname,13700,ujo-city,1958/05/14,louh-street,22,MP,NORTHERN MARIANA ISLANDS +137,mpgcz-name,ysk-firstname,11860,eva-city,1978/08/23,nedx-street,101,MT,MONTANA +138,bntsw-name,osn-firstname,15700,mmv-city,1966/07/28,loqs-street,34,WY,WYOMING +139,yfcbv-name,ing-firstname,10300,ddb-city,1966/04/12,amuj-street,32,SD,SOUTH DAKOTA +140,ddwqy-name,rxo-firstname,18720,nsh-city,1974/06/22,rugn-street,105,LA,LOUISIANA +141,zuxwq-name,xha-firstname,12240,jii-city,1974/04/22,kawh-street,97,NJ,NEW JERSEY +142,aarej-name,dfg-firstname,14680,exu-city,1958/04/17,adua-street,11,NE,NEBRASKA +143,iezfw-name,ufb-firstname,18800,fyv-city,1970/06/05,yvao-street,53,HI,HAWAII +144,vvmzr-name,bud-firstname,15120,ggo-city,1966/07/24,ozcj-street,127,MT,MONTANA +145,bknbv-name,qrd-firstname,11500,mth-city,1970/04/16,ijle-street,143,NH,NEW HAMPSHIRE +146,bwyxl-name,fdq-firstname,13160,ngn-city,1954/07/05,nkco-street,120,DE,DELAWARE +147,lkkwb-name,yqh-firstname,19580,pwn-city,1954/10/16,rgdl-street,185,MN,MINNESOTA +148,uokzd-name,aco-firstname,13940,wyf-city,1966/02/07,lbhd-street,23,NH,NEW HAMPSHIRE +149,tdmol-name,hkb-firstname,11960,wbi-city,1970/06/03,wboh-street,59,ND,NORTH DAKOTA +150,erulk-name,xcd-firstname,11420,kzt-city,1990/02/07,bmcb-street,160,DC,DISTRICT OF COLUMBIA +151,atrip-name,mlq-firstname,14440,agk-city,1986/11/08,qhdv-street,29,IN,INDIANA +152,atiir-name,brc-firstname,11380,sas-city,1958/10/20,dwyv-street,100,AR,ARKANSAS +153,cvygb-name,kdu-firstname,17300,etl-city,1954/11/13,bdxo-street,43,MA,MASSACHUSETTS +154,jeyeq-name,yjl-firstname,12740,jgr-city,1978/06/21,aavd-street,61,NY,NEW YORK +155,wojgm-name,xdk-firstname,13340,meq-city,1982/10/20,ttix-street,61,MT,MONTANA +156,oktge-name,taf-firstname,11200,ibx-city,1990/09/05,clbk-street,70,UT,UTAH +157,cdrrm-name,dmu-firstname,11980,bqa-city,1962/06/18,owlk-street,26,NY,NEW YORK +158,jyqvl-name,rht-firstname,11120,qrk-city,1982/04/20,qbrn-street,55,WY,WYOMING +159,spfxh-name,oqv-firstname,14740,gyh-city,1970/07/08,oiin-street,59,NC,NORTH CAROLINA +160,sczwy-name,mhg-firstname,17860,izz-city,1970/08/25,xehg-street,2,NJ,NEW JERSEY +161,lklcm-name,rcy-firstname,11960,ycf-city,1982/07/04,path-street,179,NJ,NEW JERSEY +162,jcluy-name,tlk-firstname,10380,lsi-city,1970/03/17,ugqr-street,138,NJ,NEW JERSEY +163,qqdvp-name,hsh-firstname,18240,bqf-city,1982/01/01,nupe-street,153,LA,LOUISIANA +164,rxlox-name,uoi-firstname,15600,uvd-city,1954/04/03,gjmv-street,197,MA,MASSACHUSETTS +165,kjypq-name,wgt-firstname,14060,yrs-city,1978/06/09,ijks-street,144,CO,COLORADO +166,zegdj-name,fpi-firstname,13380,znp-city,1978/04/26,pdlh-street,187,KY,KENTUCKY +167,ihkkq-name,gtk-firstname,15740,qbg-city,1970/06/18,odsg-street,95,CO,COLORADO +168,yhnuk-name,uhh-firstname,16720,hoo-city,1978/06/20,vrcy-street,186,KS,KANSAS +169,ftpvt-name,ufk-firstname,13600,wat-city,1954/10/16,nxax-street,112,OR,OREGON +170,xoiyz-name,xqq-firstname,14560,kea-city,1986/08/10,bivl-street,177,MN,MINNESOTA +171,wfeuq-name,qec-firstname,16540,obq-city,1950/11/17,keyf-street,108,UT,UTAH +172,pfrmg-name,tyi-firstname,15360,tjx-city,1979/01/30,lyhr-street,78,NE,NEBRASKA +173,najqw-name,ldk-firstname,10220,bci-city,1982/04/01,qxuf-street,84,MS,MISSISSIPPI +174,qbqrg-name,zyo-firstname,13420,cdh-city,1958/06/13,gqst-street,167,WY,WYOMING +175,lnaxv-name,zwt-firstname,14740,lok-city,1962/10/06,mmdu-street,149,KY,KENTUCKY +176,tpgpm-name,qie-firstname,14960,opy-city,1958/07/14,uxfv-street,158,SC,SOUTH CAROLINA +177,nltvw-name,ahc-firstname,19520,uxf-city,1958/03/16,fwsy-street,131,CA,CALIFORNIA +178,ujfpc-name,cwd-firstname,13800,gki-city,1974/10/10,sgiq-street,12,FL,FLORIDA +179,pehcm-name,mah-firstname,15940,azs-city,1970/05/07,hvvk-street,9,PR,PUERTO RICO +180,phlqr-name,qog-firstname,12160,qvt-city,1966/09/11,isol-street,155,AZ,ARIZONA +181,bxerk-name,kxv-firstname,14180,sek-city,1982/02/18,ctwu-street,84,CA,CALIFORNIA +182,nmlqw-name,oyf-firstname,12640,tmv-city,1962/02/26,eqss-street,141,NE,NEBRASKA +183,kobcl-name,pht-firstname,15820,nky-city,1978/05/14,vfnd-street,176,PR,PUERTO RICO +184,lvbqi-name,juh-firstname,12780,rst-city,1958/12/18,wwko-street,22,MP,NORTHERN MARIANA ISLANDS +185,yqqmt-name,zrg-firstname,12780,hxs-city,1954/08/12,mdxh-street,190,MS,MISSISSIPPI +186,osbyt-name,qtk-firstname,14900,ltd-city,1990/06/21,quqn-street,59,MO,MISSOURI +187,vibab-name,vgy-firstname,19600,jxa-city,1958/09/20,czps-street,137,AR,ARKANSAS +188,vrnml-name,qmd-firstname,15860,mxe-city,1966/07/23,ybfc-street,148,DE,DELAWARE +189,thimt-name,ige-firstname,12900,dqn-city,1966/05/07,bccw-street,187,OK,OKLAHOMA +190,jdzou-name,qnd-firstname,17600,fzi-city,1958/06/12,ewtx-street,174,IN,INDIANA +191,bsvvw-name,hfa-firstname,14180,kmn-city,1974/09/19,zvdw-street,13,UT,UTAH +192,iwbao-name,qur-firstname,19500,jlk-city,1982/08/08,kllj-street,113,WA,WASHINGTON +193,xpxla-name,yzv-firstname,19020,eze-city,1954/04/22,taku-street,105,AS,AMERICAN SAMOA +194,gqugh-name,sdy-firstname,14360,pwi-city,1974/03/11,qybh-street,95,KY,KENTUCKY +195,bueoc-name,sfx-firstname,10560,xhn-city,1970/08/29,zfin-street,48,NC,NORTH CAROLINA +196,fyrln-name,fay-firstname,10820,qtd-city,1974/11/04,yrtc-street,120,MH,MARSHALL ISLANDS +197,zuhli-name,qwr-firstname,19800,nqp-city,1970/01/21,mdew-street,8,VA,VIRGINIA +198,rwplk-name,jkr-firstname,18080,khf-city,1978/02/28,ihkv-street,134,MT,MONTANA +199,ssbzy-name,azn-firstname,11440,ire-city,1954/11/16,sjou-street,55,CO,COLORADO +200,zbhue-name,ces-firstname,19840,ybc-city,1974/07/17,ktsw-street,94,ND,NORTH DAKOTA +201,tcpnt-name,tgk-firstname,19580,nox-city,1990/02/24,rmst-street,59,MS,MISSISSIPPI +202,avrpe-name,aaz-firstname,14000,anm-city,1950/09/02,ddjz-street,197,FL,FLORIDA +203,qemau-name,lbl-firstname,15620,jkx-city,1962/07/23,kxdn-street,38,PA,PENNSYLVANIA +204,xzeqe-name,bjx-firstname,12960,qiv-city,1958/09/07,yohx-street,22,VA,VIRGINIA +205,ufbqh-name,dcm-firstname,17720,tch-city,1978/10/16,sqis-street,119,NM,NEW MEXICO +206,cfwje-name,kng-firstname,15980,hmf-city,1974/09/23,timl-street,105,NV,NEVADA +207,dpswi-name,lzu-firstname,11020,mby-city,1962/10/14,stnj-street,143,UT,UTAH +208,padrh-name,yvj-firstname,17680,pqc-city,1986/11/28,xmxq-street,81,GU,GUAM +209,blcun-name,erh-firstname,16200,sgc-city,1950/10/10,sqkp-street,29,PA,PENNSYLVANIA +210,jxuox-name,ztl-firstname,13140,hox-city,1962/08/12,vxgj-street,83,KS,KANSAS +211,bcfua-name,urk-firstname,11540,mhn-city,1982/10/09,poor-street,21,VI,VIRGIN ISLANDS +212,nmccn-name,nlv-firstname,11780,dec-city,1974/07/05,txyt-street,125,FL,FLORIDA +213,cwcfl-name,nye-firstname,10620,ciu-city,1974/06/14,dumh-street,124,TX,TEXAS +214,bewhj-name,mcq-firstname,16040,vir-city,1951/01/31,uhse-street,78,MA,MASSACHUSETTS +215,owbls-name,mcq-firstname,12940,zjk-city,1962/08/21,plgy-street,185,NJ,NEW JERSEY +216,tjwtx-name,uur-firstname,13400,jqa-city,1962/05/15,hhzi-street,134,KS,KANSAS +217,aynmw-name,obp-firstname,13820,hbu-city,1974/12/09,lfeb-street,28,NV,NEVADA +218,aivbc-name,fkc-firstname,14980,mew-city,1958/05/19,rxqg-street,41,AK,ALASKA +219,nkeha-name,ddi-firstname,17680,wzu-city,1986/03/04,xtik-street,11,RI,RHODE ISLAND +220,hyyqm-name,vac-firstname,17600,gph-city,1954/04/28,rjxi-street,22,NY,NEW YORK +221,sifar-name,yth-firstname,12840,kbe-city,1982/11/18,mnje-street,5,NY,NEW YORK +222,bxlyk-name,pla-firstname,15740,zpb-city,1990/12/07,viys-street,171,HI,HAWAII +223,ifcwk-name,yfu-firstname,10000,itv-city,1982/06/16,iuya-street,77,SD,SOUTH DAKOTA +224,fherw-name,acw-firstname,13000,dxg-city,1970/09/09,zscv-street,45,VT,VERMONT +225,cvbfh-name,tbs-firstname,13160,znt-city,1958/07/20,exfl-street,171,LA,LOUISIANA +226,sxpuq-name,qdu-firstname,13000,lhm-city,1971/01/08,yooq-street,80,VT,VERMONT +227,xawor-name,glz-firstname,18160,dxx-city,1954/12/08,fjnf-street,130,FM,FEDERATED STATES OF MICRONESIA +228,dwpda-name,dtg-firstname,15380,zyz-city,1974/04/21,gozg-street,96,MT,MONTANA +229,airyv-name,oue-firstname,16900,gbm-city,1986/07/14,xfte-street,45,MP,NORTHERN MARIANA ISLANDS +230,omfog-name,zhv-firstname,17020,lep-city,1970/03/21,trww-street,128,LA,LOUISIANA +231,ddgah-name,ost-firstname,13580,ojl-city,1958/03/07,gnln-street,155,KY,KENTUCKY +232,feggq-name,cro-firstname,18780,wtj-city,1966/10/01,jesi-street,63,NM,NEW MEXICO +233,ahxvq-name,nes-firstname,11660,niu-city,1950/06/06,upyk-street,185,WA,WASHINGTON +234,gjlqf-name,mvv-firstname,19620,roc-city,1974/05/01,tsqu-street,19,MI,MICHIGAN +235,xbcip-name,vyn-firstname,10560,nru-city,1986/12/06,qxfi-street,114,WI,WISCONSIN +236,cdwuj-name,sks-firstname,12560,typ-city,1954/01/31,fkwb-street,128,DC,DISTRICT OF COLUMBIA +237,bsekx-name,wbw-firstname,14280,twm-city,1962/09/11,bxui-street,174,NM,NEW MEXICO +238,foppd-name,zlw-firstname,13580,hmg-city,1974/04/29,yiwk-street,68,MN,MINNESOTA +239,brtej-name,cqi-firstname,11000,elz-city,1982/10/16,uauh-street,23,PA,PENNSYLVANIA +240,cpklp-name,tps-firstname,11440,nsm-city,1950/10/28,cmjv-street,139,PR,PUERTO RICO +241,opbzn-name,bxz-firstname,12860,jnq-city,1966/05/08,nkuq-street,35,MP,NORTHERN MARIANA ISLANDS +242,kxkmx-name,ziy-firstname,17460,wqq-city,1974/11/05,gnha-street,192,WV,WEST VIRGINIA +243,lxpbu-name,jph-firstname,19500,fpa-city,1954/10/01,gdls-street,163,MI,MICHIGAN +244,erhwd-name,wvu-firstname,11880,iza-city,1962/08/03,ucsh-street,75,undefined,undefined +245,tjuxy-name,jzf-firstname,10580,nyq-city,1970/04/30,gbes-street,189,AR,ARKANSAS +246,piocq-name,skz-firstname,14600,xuq-city,1978/07/12,inae-street,27,ME,MAINE +247,bqjty-name,ybj-firstname,13040,jqu-city,1954/11/30,ugcn-street,159,AL,ALABAMA +248,xexrx-name,fpu-firstname,19720,ckc-city,1974/06/30,zpez-street,46,IN,INDIANA +249,auzgu-name,dam-firstname,18460,mih-city,1962/07/28,augp-street,112,MD,MARYLAND +250,xupoe-name,fdb-firstname,13440,llr-city,1986/10/01,forq-street,185,IL,ILLINOIS +251,sgely-name,pzz-firstname,15920,jya-city,1950/02/02,kypg-street,147,DC,DISTRICT OF COLUMBIA +252,onini-name,zts-firstname,18060,avs-city,1974/12/02,kxjn-street,85,TX,TEXAS +253,tyflk-name,htl-firstname,17560,bhd-city,1962/06/06,xquf-street,126,IN,INDIANA +254,chbez-name,zkj-firstname,17000,goh-city,1974/03/02,rkui-street,13,AR,ARKANSAS +255,zmmhg-name,rqb-firstname,11340,egt-city,1970/02/06,pwjj-street,6,MH,MARSHALL ISLANDS +256,gutfo-name,vki-firstname,18860,xdv-city,1986/07/11,iwkf-street,14,DC,DISTRICT OF COLUMBIA +257,eogwr-name,hnt-firstname,19840,nht-city,1990/05/02,kljr-street,18,VT,VERMONT +258,ibupi-name,ygc-firstname,18580,tvk-city,1978/08/20,xphm-street,123,CO,COLORADO +259,wqpaq-name,uwc-firstname,10780,ygl-city,1958/03/20,ncta-street,87,GU,GUAM +260,swcms-name,ljb-firstname,13560,pvt-city,1954/12/13,ovsf-street,176,MD,MARYLAND +261,xwxmt-name,qpq-firstname,13380,fzc-city,1978/10/14,ivwb-street,17,AZ,ARIZONA +262,drxej-name,msd-firstname,12720,ure-city,1986/02/15,nrqa-street,30,MT,MONTANA +263,scgpq-name,wgg-firstname,18740,xtx-city,1990/05/09,vxwl-street,29,AK,ALASKA +264,qchcm-name,fbc-firstname,14480,ymf-city,1978/03/21,cfqc-street,63,ND,NORTH DAKOTA +265,cbpxm-name,clb-firstname,16900,etx-city,1974/03/29,uzyw-street,175,OK,OKLAHOMA +266,clksy-name,mls-firstname,13560,qhs-city,1958/04/07,trwx-street,47,MO,MISSOURI +267,qwagv-name,hil-firstname,18240,atx-city,1954/01/30,glaf-street,57,MT,MONTANA +268,grkjm-name,qwy-firstname,15900,jvu-city,1970/02/26,rhdn-street,172,ME,MAINE +269,gkiis-name,xhp-firstname,14440,bgh-city,1954/01/02,btrx-street,53,FL,FLORIDA +270,nscdn-name,lfn-firstname,19900,eph-city,1958/09/29,nqao-street,171,KS,KANSAS +271,ulrrc-name,ncb-firstname,10320,cao-city,1986/10/15,lkrm-street,125,GA,GEORGIA +272,qxjcn-name,wpy-firstname,11260,rew-city,1954/12/19,tldv-street,115,CA,CALIFORNIA +273,thyyj-name,htd-firstname,19100,tae-city,1982/03/04,wbqv-street,174,PR,PUERTO RICO +274,uigiq-name,lmx-firstname,19320,bkr-city,1950/04/07,foio-street,104,OK,OKLAHOMA +275,iinbr-name,cfg-firstname,12100,qwv-city,1986/01/12,ervo-street,87,MN,MINNESOTA +276,kipbw-name,fda-firstname,16300,mlu-city,1970/03/26,yjpd-street,15,MN,MINNESOTA +277,cfrgd-name,dui-firstname,11320,jax-city,1990/04/14,zaik-street,157,ME,MAINE +278,duaza-name,xsx-firstname,11120,kkg-city,1958/02/05,bply-street,185,VI,VIRGIN ISLANDS +279,zzhqx-name,vlb-firstname,18380,vyb-city,1950/12/11,iugj-street,159,HI,HAWAII +280,kuryf-name,kib-firstname,13140,tum-city,1954/05/06,clkw-street,31,CO,COLORADO +281,daljt-name,ycr-firstname,10840,ckw-city,1982/10/29,umof-street,52,CO,COLORADO +282,xhssg-name,djc-firstname,17840,gvj-city,1954/12/12,zgmw-street,35,WV,WEST VIRGINIA +283,qqscv-name,eiu-firstname,15260,daj-city,1986/01/30,qprc-street,158,MH,MARSHALL ISLANDS +284,tehbb-name,czj-firstname,14180,xnh-city,1958/03/06,lfji-street,48,UT,UTAH +285,acdsd-name,yiu-firstname,12220,buk-city,1958/02/15,ovcj-street,70,ME,MAINE +286,derpl-name,buv-firstname,16000,fha-city,1970/09/02,vfay-street,57,WA,WASHINGTON +287,lobqd-name,qxn-firstname,15060,ycp-city,1966/12/24,axxb-street,38,PA,PENNSYLVANIA +288,owxja-name,okb-firstname,15640,lad-city,1982/05/24,wnka-street,101,MN,MINNESOTA +289,zohkw-name,ypo-firstname,15460,wtu-city,1978/08/10,jhco-street,21,VA,VIRGINIA +290,nmrar-name,aqm-firstname,18360,lcn-city,1963/01/26,zxeo-street,143,WV,WEST VIRGINIA +291,tndrh-name,ael-firstname,16360,cyb-city,1958/04/10,cmlg-street,78,ND,NORTH DAKOTA +292,kbpkv-name,yck-firstname,19400,oka-city,1974/11/02,kpmc-street,10,CO,COLORADO +293,uabcl-name,zms-firstname,15940,tzb-city,1970/08/06,ezzs-street,11,VT,VERMONT +294,lancp-name,zbk-firstname,11560,vny-city,1978/07/21,tzys-street,182,LA,LOUISIANA +295,wagzv-name,hcp-firstname,13760,kik-city,1974/11/01,gpuw-street,151,NH,NEW HAMPSHIRE +296,qrenr-name,egp-firstname,16920,zwq-city,1966/03/19,fbwi-street,102,FL,FLORIDA +297,amjep-name,mds-firstname,16700,fvb-city,1978/05/07,peau-street,167,WV,WEST VIRGINIA +298,dzppv-name,qav-firstname,16680,wbc-city,1970/04/15,dzyp-street,149,VI,VIRGIN ISLANDS +299,qrlwz-name,hvk-firstname,14640,qyl-city,1954/03/07,gvsc-street,32,NC,NORTH CAROLINA +300,lracx-name,dmp-firstname,13800,slo-city,1970/09/21,jspb-street,106,WA,WASHINGTON +301,jhlao-name,txt-firstname,12020,jam-city,1962/06/26,bcky-street,19,WV,WEST VIRGINIA +302,aocxq-name,mzv-firstname,17140,dgx-city,1954/08/24,maoi-street,1,HI,HAWAII +303,dvdbu-name,fdf-firstname,10980,goa-city,1982/04/10,onik-street,109,NM,NEW MEXICO +304,cqapx-name,skq-firstname,11080,akw-city,1958/09/02,xakx-street,14,CA,CALIFORNIA +305,crmlf-name,djf-firstname,16100,mle-city,1974/10/10,udrl-street,96,VT,VERMONT +306,iujcn-name,tat-firstname,17660,jnf-city,1966/03/31,inoh-street,104,TX,TEXAS +307,aetvh-name,spn-firstname,14960,wxf-city,1954/06/30,mmxe-street,78,PA,PENNSYLVANIA +308,ibfdt-name,sfu-firstname,11120,kqf-city,1954/06/22,xbwg-street,132,CA,CALIFORNIA +309,ccatv-name,aeb-firstname,10940,qoi-city,1982/08/11,bsrg-street,29,UT,UTAH +310,udrjw-name,agc-firstname,17100,aep-city,1974/04/23,bsju-street,59,PR,PUERTO RICO +311,crjcx-name,nbb-firstname,14820,xtp-city,1958/11/08,fajh-street,95,WY,WYOMING +312,qbcrw-name,mef-firstname,14220,mwa-city,1982/11/13,keyy-street,97,NE,NEBRASKA +313,wxvpi-name,dym-firstname,10220,ncp-city,1954/09/28,qgrg-street,25,undefined,undefined +314,bandc-name,hzf-firstname,18700,eoh-city,1990/11/05,qphi-street,177,IL,ILLINOIS +315,afatf-name,eii-firstname,15480,lsm-city,1982/10/11,nzjq-street,96,VI,VIRGIN ISLANDS +316,trxge-name,vhe-firstname,18260,ccm-city,1958/03/09,ducm-street,1,NH,NEW HAMPSHIRE +317,kdygt-name,tgc-firstname,12500,bqo-city,1978/06/20,rqgx-street,90,DC,DISTRICT OF COLUMBIA +318,angdt-name,lvt-firstname,13960,tbr-city,1974/07/14,wfqj-street,196,GU,GUAM +319,hqhqe-name,nxd-firstname,14860,yhi-city,1954/04/23,wkpi-street,23,MP,NORTHERN MARIANA ISLANDS +320,pwwep-name,qtw-firstname,15160,utn-city,1958/06/08,sheh-street,176,FL,FLORIDA +321,vkken-name,wrw-firstname,16760,jhm-city,1974/12/13,czmf-street,183,AR,ARKANSAS +322,zbbmz-name,ipe-firstname,14340,mco-city,1970/04/25,ymou-street,2,WY,WYOMING +323,eyzfr-name,xeb-firstname,10440,qsa-city,1990/04/15,iukl-street,135,AZ,ARIZONA +324,tfpxy-name,yzh-firstname,14080,wbc-city,1970/02/12,ojfc-street,150,MP,NORTHERN MARIANA ISLANDS +325,tphou-name,xac-firstname,15940,ncy-city,1962/09/22,vcxl-street,90,AL,ALABAMA +326,bfscx-name,yih-firstname,18900,bxa-city,1962/10/12,qsww-street,137,VT,VERMONT +327,ussbw-name,gbb-firstname,15600,wtg-city,1978/07/29,vqcq-street,10,KY,KENTUCKY +328,zkoqj-name,mqn-firstname,13440,lux-city,1970/05/01,bokx-street,106,VA,VIRGINIA +329,zbqew-name,dbw-firstname,14520,hbi-city,1971/01/20,syea-street,192,CT,CONNECTICUT +330,vmfuy-name,qge-firstname,16660,vnk-city,1982/08/29,tlxy-street,166,PW,PALAU +331,wajgr-name,mnx-firstname,17500,wiv-city,1954/07/10,ylug-street,21,OR,OREGON +332,lzcry-name,tzk-firstname,18980,icz-city,1966/06/04,ldvu-street,25,GU,GUAM +333,rodpv-name,rix-firstname,16540,tpf-city,1986/02/03,leur-street,169,SD,SOUTH DAKOTA +334,pxmhq-name,tyd-firstname,11200,okj-city,1978/12/30,tauk-street,94,ND,NORTH DAKOTA +335,kansg-name,qzs-firstname,18020,yhj-city,1982/08/27,ehie-street,140,AR,ARKANSAS +336,kpqff-name,bqs-firstname,13780,tnp-city,1958/08/07,euhw-street,182,SD,SOUTH DAKOTA +337,akvdd-name,bxi-firstname,15620,rbk-city,1962/03/16,fzdy-street,125,WA,WASHINGTON +338,nbazl-name,ikv-firstname,13160,wci-city,1966/08/22,smzb-street,136,SD,SOUTH DAKOTA +339,zrowi-name,udo-firstname,16460,hbe-city,1962/07/07,xgyd-street,170,AZ,ARIZONA +340,jfzjd-name,atn-firstname,13040,yef-city,1974/09/26,dyjj-street,127,WI,WISCONSIN +341,prsbo-name,ibh-firstname,19520,qro-city,1986/01/10,pemg-street,183,SC,SOUTH CAROLINA +342,dwfdn-name,kci-firstname,14640,fze-city,1970/07/24,xxdz-street,102,IA,IOWA +343,cnupm-name,rjl-firstname,14660,ewk-city,1966/07/02,wzjr-street,107,PW,PALAU +344,wbpor-name,fuf-firstname,11300,xne-city,1978/02/14,tyzx-street,2,AL,ALABAMA +345,tjqky-name,mjf-firstname,12560,qlo-city,1982/10/21,hzos-street,121,NE,NEBRASKA +346,yxqsn-name,ofx-firstname,18680,ute-city,1974/02/27,wqka-street,111,FL,FLORIDA +347,wjwmv-name,hra-firstname,14520,mvj-city,1954/10/03,chmz-street,70,SD,SOUTH DAKOTA +348,aipnn-name,paa-firstname,14400,gyq-city,1970/04/27,tbpq-street,84,undefined,undefined +349,wplyl-name,zvh-firstname,15820,vhw-city,1967/01/12,eesj-street,110,undefined,undefined +350,syoip-name,lbd-firstname,13860,wfo-city,1966/03/17,lsvf-street,47,VI,VIRGIN ISLANDS +351,lvhdk-name,pzl-firstname,14320,bab-city,1950/05/01,wktz-street,134,AR,ARKANSAS +352,jodya-name,apd-firstname,14520,nfr-city,1986/12/14,jgzs-street,193,KY,KENTUCKY +353,ikunj-name,syx-firstname,17320,cqd-city,1982/08/19,xwzj-street,53,IN,INDIANA +354,bnhud-name,uvd-firstname,15360,ynw-city,1971/01/25,gens-street,107,MT,MONTANA +355,prkrf-name,ivm-firstname,19220,kie-city,1966/11/14,boem-street,59,HI,HAWAII +356,ucywi-name,sjl-firstname,11480,ish-city,1982/11/13,szck-street,148,NE,NEBRASKA +357,sgftd-name,ajp-firstname,11760,jco-city,1958/03/05,djek-street,196,UT,UTAH +358,kohdq-name,phr-firstname,13120,kpb-city,1978/07/26,csom-street,17,PR,PUERTO RICO +359,fgjoa-name,srp-firstname,11480,onn-city,1990/05/26,duvo-street,127,MS,MISSISSIPPI +360,xgiyw-name,rcu-firstname,15140,mti-city,1958/09/26,lego-street,125,MA,MASSACHUSETTS +361,kgcui-name,grq-firstname,12640,tjn-city,1986/02/08,ntoq-street,6,CT,CONNECTICUT +362,ljyxw-name,lho-firstname,19100,hmd-city,1962/03/09,mqus-street,12,CT,CONNECTICUT +363,vnvwy-name,vmv-firstname,14360,cpu-city,1966/04/20,cfts-street,16,MI,MICHIGAN +364,gedwu-name,ocl-firstname,11760,vaf-city,1970/04/22,exot-street,121,MT,MONTANA +365,enknw-name,vjk-firstname,11100,bge-city,1954/07/06,inhb-street,95,AS,AMERICAN SAMOA +366,qmpom-name,tmp-firstname,12260,ymn-city,1986/07/02,psdj-street,71,MI,MICHIGAN +367,jrbwj-name,imr-firstname,11080,qsx-city,1966/11/08,msys-street,80,ID,IDAHO +368,dzjpp-name,cwl-firstname,15720,ipm-city,1958/07/13,acsb-street,87,IL,ILLINOIS +369,qdwdn-name,dtc-firstname,12740,qab-city,1986/11/29,besk-street,80,DC,DISTRICT OF COLUMBIA +370,gbeyp-name,lzl-firstname,19740,ljz-city,1974/10/01,zwga-street,52,RI,RHODE ISLAND +371,kuwva-name,noq-firstname,17500,xia-city,1950/03/18,omzn-street,164,KS,KANSAS +372,jjhsm-name,cdc-firstname,13020,xli-city,1986/06/10,nups-street,38,VA,VIRGINIA +373,xyupl-name,fyd-firstname,16100,fqd-city,1971/01/05,icjo-street,28,NE,NEBRASKA +374,ueipj-name,meb-firstname,19880,nsk-city,1974/08/15,aqwr-street,14,LA,LOUISIANA +375,vdmif-name,fat-firstname,19120,dud-city,1974/07/01,krgw-street,178,MS,MISSISSIPPI +376,oxgdf-name,apn-firstname,11400,dji-city,1962/04/02,ttnt-street,13,HI,HAWAII +377,xgxyc-name,jrn-firstname,19400,bfg-city,1950/12/30,jzgy-street,43,WY,WYOMING +378,bczfk-name,bfu-firstname,16920,qxp-city,1974/05/26,seja-street,178,HI,HAWAII +379,pppqe-name,kwo-firstname,15260,geb-city,1986/04/06,okvv-street,11,MN,MINNESOTA +380,opzyy-name,mfk-firstname,14960,nlo-city,1962/03/03,dezo-street,106,AS,AMERICAN SAMOA +381,xmnsm-name,ckj-firstname,10400,fnr-city,1950/10/21,uhme-street,154,NC,NORTH CAROLINA +382,moqtn-name,zgw-firstname,12920,pqk-city,1950/10/28,suvi-street,102,WY,WYOMING +383,byapu-name,pix-firstname,10900,gik-city,1982/09/04,ntiq-street,45,VI,VIRGIN ISLANDS +384,zuhdb-name,gbj-firstname,18760,vkk-city,1978/06/17,vnem-street,62,TX,TEXAS +385,gkjpo-name,qwq-firstname,12380,ame-city,1982/12/01,ndvp-street,94,VA,VIRGINIA +386,yefwf-name,aev-firstname,13580,mor-city,1962/07/09,ldcg-street,91,AL,ALABAMA +387,twgsp-name,mwx-firstname,15840,bbp-city,1970/05/12,hecl-street,137,WY,WYOMING +388,zatdb-name,tes-firstname,17080,yga-city,1954/11/05,dfwu-street,58,undefined,undefined +389,qxdyx-name,tum-firstname,18100,mqw-city,1958/10/21,gndl-street,156,FL,FLORIDA +390,ncpki-name,wbm-firstname,13860,cuo-city,1970/07/02,yqpa-street,30,NE,NEBRASKA +391,kyzsl-name,djj-firstname,17400,zzd-city,1978/02/11,mvkp-street,4,ME,MAINE +392,jtksy-name,ayp-firstname,10760,gui-city,1982/12/17,wohf-street,175,DC,DISTRICT OF COLUMBIA +393,grzby-name,oyz-firstname,15140,bmz-city,1974/07/10,rrui-street,60,NC,NORTH CAROLINA +394,sgaut-name,xzd-firstname,15260,dnb-city,1978/10/01,hcii-street,169,RI,RHODE ISLAND +395,ozkaq-name,brx-firstname,11400,guw-city,1962/02/08,pswz-street,194,NH,NEW HAMPSHIRE +396,uqivg-name,map-firstname,10880,lgr-city,1990/07/14,lxmi-street,143,MN,MINNESOTA +397,soqkg-name,jws-firstname,17200,dss-city,1970/12/05,ppht-street,187,UT,UTAH +398,pdqdm-name,erh-firstname,11860,obj-city,1986/04/03,aova-street,121,CT,CONNECTICUT +399,ogqrv-name,uyf-firstname,16400,abs-city,1987/01/06,xwue-street,114,AZ,ARIZONA +400,ukcxw-name,ltd-firstname,11760,bwi-city,1986/09/01,ddjt-street,199,AR,ARKANSAS +401,djcgr-name,tet-firstname,19380,ure-city,1962/09/03,alzp-street,169,SC,SOUTH CAROLINA +402,ibbvs-name,cyv-firstname,16060,gdh-city,1982/04/24,qrdz-street,116,NV,NEVADA +403,blmke-name,jtq-firstname,17260,tls-city,1970/05/28,eylx-street,171,NV,NEVADA +404,qpatk-name,mtt-firstname,13400,nzv-city,1970/03/25,exby-street,93,WY,WYOMING +405,mbngf-name,pqp-firstname,10720,ann-city,1966/09/01,rkpu-street,146,CT,CONNECTICUT +406,rcydx-name,usf-firstname,12880,jou-city,1982/02/20,gtqw-street,186,MO,MISSOURI +407,srszq-name,dtb-firstname,14340,yrs-city,1978/03/16,wtan-street,89,MN,MINNESOTA +408,ezlfr-name,ebd-firstname,19420,ctw-city,1986/03/22,zjyv-street,44,VT,VERMONT +409,bhotj-name,hzt-firstname,16480,rbt-city,1978/06/11,mbsm-street,157,WV,WEST VIRGINIA +410,udzwf-name,kfd-firstname,13440,maj-city,1986/06/06,kerz-street,180,AZ,ARIZONA +411,yflfv-name,zdl-firstname,13300,zix-city,1982/05/20,ozcp-street,153,MN,MINNESOTA +412,wlyyq-name,kdz-firstname,11400,ygb-city,1966/11/07,zekv-street,111,AS,AMERICAN SAMOA +413,wonzh-name,eeb-firstname,19920,qnt-city,1982/09/20,fxob-street,95,LA,LOUISIANA +414,eiwzh-name,hpm-firstname,15860,bfo-city,1967/01/30,tkpg-street,182,VA,VIRGINIA +415,mbqgk-name,jsm-firstname,15040,dai-city,1954/06/12,jqdh-street,65,NE,NEBRASKA +416,jhigj-name,qyn-firstname,18360,ntm-city,1974/05/15,eghd-street,188,ME,MAINE +417,llytl-name,jqe-firstname,11280,kkp-city,1974/04/03,oudg-street,69,DE,DELAWARE +418,urhgl-name,iji-firstname,13760,lhf-city,1962/12/02,oywx-street,6,SC,SOUTH CAROLINA +419,uflrm-name,hef-firstname,15040,usl-city,1974/11/06,qvgi-street,103,OK,OKLAHOMA +420,gbfui-name,goz-firstname,14940,edu-city,1986/02/25,gkqy-street,26,KS,KANSAS +421,nysbp-name,fro-firstname,18420,wqt-city,1958/12/19,vpoc-street,89,GA,GEORGIA +422,fmrwo-name,wlf-firstname,17820,qwb-city,1962/05/03,stcz-street,22,CA,CALIFORNIA +423,racwd-name,kqr-firstname,10180,xdr-city,1974/04/09,fqxz-street,15,MO,MISSOURI +424,jpopz-name,krm-firstname,13420,fjx-city,1970/10/29,kyph-street,54,NJ,NEW JERSEY +425,fwdat-name,ppn-firstname,15660,zqh-city,1986/06/24,zgfe-street,61,SC,SOUTH CAROLINA +426,orznz-name,hyy-firstname,12020,lju-city,1982/07/01,xisy-street,125,GU,GUAM +427,spzxo-name,mpv-firstname,13220,cbq-city,1962/05/09,qlqx-street,53,OK,OKLAHOMA +428,qxdra-name,ifp-firstname,10480,nvu-city,1958/08/15,egsq-street,133,MH,MARSHALL ISLANDS +429,yhelu-name,jsc-firstname,17200,clp-city,1954/12/01,vahx-street,3,NV,NEVADA +430,umvfv-name,mbe-firstname,13600,knp-city,1954/07/16,oldf-street,188,PA,PENNSYLVANIA +431,mwcfe-name,xxi-firstname,15080,chq-city,1974/12/22,kpsj-street,163,NV,NEVADA +432,wvkrp-name,dtr-firstname,19840,pqv-city,1986/07/07,jnzd-street,119,HI,HAWAII +433,vqfja-name,kep-firstname,19380,ydo-city,1966/11/11,gsfd-street,12,AR,ARKANSAS +434,ikbjr-name,ipd-firstname,17920,uld-city,1990/03/03,tmbc-street,49,KS,KANSAS +435,hyjrs-name,jqp-firstname,18820,vvm-city,1970/05/07,txye-street,152,AR,ARKANSAS +436,tuewv-name,lkd-firstname,17060,slo-city,1970/07/06,htdm-street,197,OK,OKLAHOMA +437,muyws-name,iql-firstname,19540,cwf-city,1962/04/27,knsx-street,167,LA,LOUISIANA +438,tgsga-name,lww-firstname,17780,goh-city,1982/12/24,lzcl-street,136,SD,SOUTH DAKOTA +439,agsyj-name,fve-firstname,19260,wgi-city,1970/03/07,aone-street,48,WV,WEST VIRGINIA +440,yrgqp-name,tni-firstname,15820,mzp-city,1986/04/19,femc-street,29,PR,PUERTO RICO +441,ltjmw-name,cps-firstname,13060,aeo-city,1954/07/17,bewv-street,182,OK,OKLAHOMA +442,gjkpl-name,tre-firstname,16340,ndn-city,1962/08/27,ocld-street,16,WV,WEST VIRGINIA +443,ryvgz-name,bhe-firstname,14960,lcg-city,1978/08/09,bxwa-street,19,NM,NEW MEXICO +444,zgwwi-name,umb-firstname,11840,llj-city,1958/07/06,uiww-street,115,MP,NORTHERN MARIANA ISLANDS +445,qaczz-name,qng-firstname,10900,umr-city,1970/01/17,mlsy-street,6,VA,VIRGINIA +446,xcruf-name,vbp-firstname,17840,vzl-city,1982/06/17,oatg-street,110,MN,MINNESOTA +447,egqdv-name,boy-firstname,18360,gfw-city,1958/10/07,qaoh-street,104,AS,AMERICAN SAMOA +448,digvt-name,fid-firstname,11180,iyw-city,1958/03/13,pooo-street,119,NV,NEVADA +449,gxavv-name,bal-firstname,17020,cra-city,1966/02/01,nchf-street,122,CO,COLORADO +450,tiwoz-name,vwo-firstname,19340,oja-city,1982/07/16,bnsy-street,149,MA,MASSACHUSETTS +451,jemtu-name,gnk-firstname,11180,gbb-city,1954/03/01,pmbh-street,87,NC,NORTH CAROLINA +452,rhdjl-name,qaf-firstname,16500,nqr-city,1974/03/28,vmrd-street,75,DE,DELAWARE +453,qdkbn-name,cpl-firstname,17460,ugb-city,1987/01/29,cwoa-street,184,ME,MAINE +454,tuhon-name,bbg-firstname,10500,cer-city,1958/03/02,ttst-street,184,WA,WASHINGTON +455,wluxg-name,xpv-firstname,18240,axq-city,1958/07/28,useg-street,140,NY,NEW YORK +456,ojkgh-name,tzq-firstname,16000,guv-city,1990/11/25,hagy-street,198,MN,MINNESOTA +457,arxsx-name,ana-firstname,18620,oxx-city,1986/04/22,hkdb-street,28,SD,SOUTH DAKOTA +458,ujpiw-name,bty-firstname,14000,kiy-city,1978/08/10,bmgt-street,176,PW,PALAU +459,sufuk-name,izq-firstname,17740,kfy-city,1986/10/29,xxcz-street,26,MD,MARYLAND +460,nedie-name,ajz-firstname,11820,fnf-city,1970/04/08,ccnd-street,108,MT,MONTANA +461,vbfwv-name,anp-firstname,19880,qag-city,1962/11/17,tbcc-street,23,NJ,NEW JERSEY +462,bmrzz-name,yfe-firstname,11300,rgi-city,1970/10/01,xbjp-street,26,FM,FEDERATED STATES OF MICRONESIA +463,jewvw-name,ymh-firstname,13100,kcv-city,1982/03/01,cjbt-street,4,KY,KENTUCKY +464,kxxpm-name,has-firstname,18500,hlp-city,1954/04/03,qsmq-street,77,MD,MARYLAND +465,qkjxa-name,gdq-firstname,12240,qtz-city,1954/11/25,mevz-street,12,NM,NEW MEXICO +466,vmdwq-name,vjm-firstname,19980,rmz-city,1986/09/09,ifxg-street,139,NC,NORTH CAROLINA +467,ssgil-name,lkd-firstname,14220,ndd-city,1963/01/19,rjln-street,195,MD,MARYLAND +468,szbhe-name,pwi-firstname,10100,iij-city,1966/04/09,mojp-street,177,AK,ALASKA +469,eswrp-name,lts-firstname,10080,cuk-city,1966/03/29,plor-street,139,VI,VIRGIN ISLANDS +470,tioeh-name,qgc-firstname,11800,zre-city,1954/06/05,owaq-street,98,LA,LOUISIANA +471,fkzpf-name,bse-firstname,19040,cor-city,1962/06/21,aamy-street,53,NY,NEW YORK +472,kczhq-name,hde-firstname,16380,siz-city,1986/12/15,rawc-street,127,ND,NORTH DAKOTA +473,gpwqf-name,pae-firstname,12820,rga-city,1958/12/19,djsk-street,131,WY,WYOMING +474,yvgmq-name,hzp-firstname,19020,ioc-city,1966/03/22,zdbt-street,106,FM,FEDERATED STATES OF MICRONESIA +475,abjqk-name,pdo-firstname,13040,vnj-city,1962/09/08,kvwv-street,145,OR,OREGON +476,adppx-name,gmz-firstname,16560,pah-city,1962/03/14,ynqs-street,107,WY,WYOMING +477,lxcrs-name,arg-firstname,12160,med-city,1990/03/14,wlag-street,141,MT,MONTANA +478,mrkfp-name,jbm-firstname,19760,dhu-city,1970/06/12,idan-street,145,UT,UTAH +479,zmkad-name,vns-firstname,10080,aoe-city,1955/01/25,qgbd-street,174,FL,FLORIDA +480,vftgh-name,nxs-firstname,11580,igp-city,1954/05/17,fief-street,183,MT,MONTANA +481,bnafy-name,geg-firstname,11160,sfp-city,1974/04/26,tpnq-street,194,DE,DELAWARE +482,mwqpn-name,lbw-firstname,17660,oot-city,1974/04/09,qxgk-street,18,AK,ALASKA +483,cijcf-name,uvd-firstname,17940,ioy-city,1982/02/07,gfiz-street,147,MN,MINNESOTA +484,kwjhv-name,swd-firstname,12400,pue-city,1970/12/10,sall-street,104,MN,MINNESOTA +485,mfdfy-name,hsy-firstname,18260,bzl-city,1982/09/08,hsyc-street,76,RI,RHODE ISLAND +486,cdrmm-name,mxa-firstname,11520,rie-city,1962/12/21,dxed-street,112,LA,LOUISIANA +487,flyur-name,mzm-firstname,12260,wyi-city,1978/07/07,xqoj-street,156,WV,WEST VIRGINIA +488,hkwnf-name,obg-firstname,14520,bib-city,1967/01/20,mvee-street,115,undefined,undefined +489,kawax-name,pyn-firstname,10520,zjh-city,1966/09/01,fsmz-street,87,DE,DELAWARE +490,nnhzs-name,sfo-firstname,19040,thn-city,1990/11/08,tzsb-street,43,IN,INDIANA +491,xivec-name,gzo-firstname,16820,aha-city,1978/10/19,iolt-street,36,HI,HAWAII +492,hyiju-name,plw-firstname,18620,zzu-city,1982/07/13,tydq-street,112,NV,NEVADA +493,leylb-name,fcv-firstname,15720,biw-city,1958/09/18,bnpf-street,52,MT,MONTANA +494,pweci-name,hcu-firstname,19020,fzb-city,1950/10/28,spdv-street,131,WY,WYOMING +495,ddulq-name,crh-firstname,11540,yrm-city,1974/07/31,opds-street,95,OH,OHIO +496,fyrha-name,wea-firstname,16620,vfe-city,1990/12/07,ukki-street,48,GU,GUAM +497,vuypf-name,ugz-firstname,13320,ixw-city,1970/07/09,aptu-street,60,HI,HAWAII +498,wezwd-name,oae-firstname,11180,egb-city,1982/02/27,ldea-street,2,IL,ILLINOIS +499,wrokv-name,zaa-firstname,13980,hac-city,1974/01/20,zwst-street,21,MO,MISSOURI +500,wtapc-name,ciu-firstname,15360,uvh-city,1962/08/03,mddz-street,196,IA,IOWA +501,hdtsu-name,two-firstname,19500,ick-city,1958/11/07,kipe-street,42,DE,DELAWARE +502,lwprl-name,jaw-firstname,14140,acy-city,1970/12/06,jhae-street,129,MT,MONTANA +503,iwftp-name,jnp-firstname,18800,axg-city,1978/10/13,rejy-street,174,AZ,ARIZONA +504,qypws-name,fox-firstname,19820,bzu-city,1966/01/02,owsq-street,70,AR,ARKANSAS +505,pzjcf-name,ese-firstname,12180,kjq-city,1958/11/21,xbyg-street,12,AS,AMERICAN SAMOA +506,itzxd-name,erv-firstname,10460,dsk-city,1978/06/20,baci-street,151,OH,OHIO +507,jnpdw-name,zna-firstname,11000,aqt-city,1966/05/27,pukm-street,80,WY,WYOMING +508,dchwr-name,rxe-firstname,19220,plm-city,1958/05/18,gkgx-street,100,NH,NEW HAMPSHIRE +509,pcszz-name,rym-firstname,15860,tml-city,1983/01/25,qrdz-street,7,RI,RHODE ISLAND +510,fatdr-name,rcs-firstname,17480,ajx-city,1958/09/18,nlal-street,26,MD,MARYLAND +511,oblzo-name,wwl-firstname,17280,hxs-city,1958/06/15,rnpa-street,20,AR,ARKANSAS +512,nmflo-name,ljc-firstname,19720,biq-city,1962/03/23,ypux-street,197,OR,OREGON +513,ajhdh-name,iba-firstname,16920,yru-city,1982/09/08,zedq-street,148,MN,MINNESOTA +514,aiewz-name,gla-firstname,19340,zvj-city,1986/01/12,blie-street,116,MA,MASSACHUSETTS +515,tglnr-name,fob-firstname,14300,ejm-city,1986/09/22,zazt-street,152,WV,WEST VIRGINIA +516,tswnt-name,aal-firstname,19940,jsw-city,1978/07/02,xnjc-street,125,OK,OKLAHOMA +517,smukz-name,zim-firstname,15260,pul-city,1974/09/26,furv-street,45,IA,IOWA +518,aygln-name,qfk-firstname,16700,bmu-city,1958/03/18,esys-street,148,NE,NEBRASKA +519,kcuub-name,ffc-firstname,10720,xnk-city,1958/02/23,cefn-street,135,AL,ALABAMA +520,vsujm-name,yne-firstname,11280,gdr-city,1966/02/12,hdah-street,70,MS,MISSISSIPPI +521,tirxh-name,gpy-firstname,19360,bai-city,1970/04/19,gznh-street,33,KY,KENTUCKY +522,wlqnf-name,nnd-firstname,16120,kij-city,1954/10/07,gorj-street,85,ID,IDAHO +523,rynel-name,iaq-firstname,13640,chy-city,1954/02/01,wbiu-street,62,WV,WEST VIRGINIA +524,ohcvr-name,eod-firstname,18240,lcc-city,1978/12/24,guca-street,84,LA,LOUISIANA +525,gabiw-name,gtj-firstname,17860,ezv-city,1978/01/18,jsyv-street,143,ND,NORTH DAKOTA +526,ddajc-name,cab-firstname,12920,fgz-city,1958/10/06,lkvp-street,80,NV,NEVADA +527,kivtx-name,pbs-firstname,17980,mso-city,1982/10/22,qden-street,69,IN,INDIANA +528,tyial-name,mwb-firstname,17080,pdf-city,1954/02/25,qyym-street,48,MN,MINNESOTA +529,ipbyo-name,lcr-firstname,12040,ygz-city,1978/03/01,qbqk-street,117,ND,NORTH DAKOTA +530,tmkxn-name,jhl-firstname,13460,xkh-city,1966/06/07,wkzu-street,26,SD,SOUTH DAKOTA +531,cnkac-name,jxe-firstname,13140,vgf-city,1974/08/24,sifo-street,84,ID,IDAHO +532,zqnwe-name,ujv-firstname,19020,vjy-city,1966/02/03,sfzz-street,171,MN,MINNESOTA +533,aphod-name,vsz-firstname,11440,spc-city,1962/07/28,alnl-street,152,FM,FEDERATED STATES OF MICRONESIA +534,tjber-name,uhp-firstname,19320,quo-city,1958/12/19,zgus-street,173,FM,FEDERATED STATES OF MICRONESIA +535,itblo-name,lbp-firstname,14840,rgn-city,1962/11/29,qmtb-street,1,IN,INDIANA +536,navpk-name,wcs-firstname,14400,nvr-city,1982/11/01,qlar-street,126,AK,ALASKA +537,nkyot-name,zep-firstname,17720,yim-city,1954/09/30,wwdl-street,198,ND,NORTH DAKOTA +538,qebao-name,edw-firstname,19220,tlh-city,1982/10/16,celb-street,180,NJ,NEW JERSEY +539,bzmvi-name,roq-firstname,10240,beg-city,1974/07/10,nnom-street,71,KY,KENTUCKY +540,qtzxt-name,nau-firstname,17240,fis-city,1990/05/27,fbex-street,22,AR,ARKANSAS +541,jarpd-name,mce-firstname,17060,rgw-city,1982/10/20,eqbd-street,180,WI,WISCONSIN +542,ppaal-name,afq-firstname,17300,fno-city,1982/10/01,zhby-street,108,NY,NEW YORK +543,sejhl-name,qwc-firstname,18000,sfv-city,1986/04/13,xhlx-street,52,CO,COLORADO +544,qfvqk-name,mqn-firstname,10540,rwu-city,1958/07/30,xwsy-street,169,AL,ALABAMA +545,ndowg-name,lnm-firstname,13820,llz-city,1954/08/11,niif-street,55,KS,KANSAS +546,hutga-name,ztk-firstname,19800,uci-city,1950/10/04,ywtn-street,84,KS,KANSAS +547,mudul-name,qcs-firstname,13520,agk-city,1962/04/20,scto-street,200,CO,COLORADO +548,mnket-name,edm-firstname,10040,jqb-city,1954/11/09,avkk-street,23,VA,VIRGINIA +549,mbxbn-name,yuu-firstname,18880,oho-city,1970/11/24,heug-street,116,PA,PENNSYLVANIA +550,letcc-name,pyw-firstname,10160,lzb-city,1966/06/11,wtul-street,80,RI,RHODE ISLAND +551,jqovo-name,eir-firstname,14120,bvr-city,1974/07/01,wdxz-street,156,SD,SOUTH DAKOTA +552,jdkan-name,vxc-firstname,15600,rqa-city,1962/02/07,vroo-street,182,CO,COLORADO +553,crzen-name,vwe-firstname,18800,msz-city,1962/10/17,xxbp-street,165,IA,IOWA +554,nogoy-name,swt-firstname,11580,npe-city,1978/04/15,uath-street,55,NV,NEVADA +555,qtkar-name,cdv-firstname,17020,opk-city,1950/07/26,bsnt-street,17,AK,ALASKA +556,oigrk-name,zmz-firstname,14200,zhd-city,1966/11/30,yeqk-street,12,DC,DISTRICT OF COLUMBIA +557,qspmb-name,htl-firstname,10940,oxl-city,1986/11/17,bqdp-street,71,NH,NEW HAMPSHIRE +558,qktka-name,ces-firstname,12840,xcq-city,1986/02/13,ddqq-street,62,NE,NEBRASKA +559,bokzl-name,vfw-firstname,14160,eyd-city,1970/02/15,ffrm-street,26,IL,ILLINOIS +560,eksje-name,xxw-firstname,18600,vtn-city,1962/12/09,nskq-street,42,VA,VIRGINIA +561,avxuw-name,niq-firstname,13520,cyx-city,1958/05/29,dxit-street,167,CA,CALIFORNIA +562,xwskh-name,mud-firstname,10900,bcd-city,1954/02/15,lqcd-street,5,VA,VIRGINIA +563,jrlgi-name,qbl-firstname,11780,xkb-city,1970/01/27,wfti-street,101,CO,COLORADO +564,vvgef-name,qbi-firstname,17580,dgu-city,1974/10/21,pkhw-street,140,DE,DELAWARE +565,vbdsa-name,mqy-firstname,19520,iqf-city,1958/06/24,gnhg-street,23,AL,ALABAMA +566,mimxv-name,idp-firstname,19280,acp-city,1986/02/10,wziw-street,1,LA,LOUISIANA +567,ihvtb-name,jdf-firstname,17540,xpg-city,1970/11/23,pjvm-street,156,SC,SOUTH CAROLINA +568,wztgo-name,njm-firstname,15200,btu-city,1978/10/30,ihdb-street,25,OH,OHIO +569,unnnh-name,zry-firstname,12300,upy-city,1982/09/15,nbjp-street,91,GU,GUAM +570,qtsep-name,spo-firstname,15940,ryi-city,1983/01/26,kvcq-street,94,FM,FEDERATED STATES OF MICRONESIA +571,rznvs-name,cjh-firstname,15180,duz-city,1974/05/12,igjl-street,111,AL,ALABAMA +572,yrdmb-name,lvl-firstname,12240,pzi-city,1962/11/10,kegb-street,54,NY,NEW YORK +573,jdhso-name,hms-firstname,12620,ova-city,1950/02/14,rxks-street,115,MS,MISSISSIPPI +574,seigu-name,rwp-firstname,15620,yrt-city,1958/09/30,mloc-street,9,NJ,NEW JERSEY +575,wzura-name,fzo-firstname,12500,vgt-city,1974/06/01,tevl-street,50,NC,NORTH CAROLINA +576,urqhu-name,hiq-firstname,15020,ejs-city,1966/04/08,khev-street,38,AK,ALASKA +577,yazce-name,zhy-firstname,12920,ytz-city,1982/05/07,rfsh-street,125,NH,NEW HAMPSHIRE +578,okxei-name,qbi-firstname,17400,plc-city,1966/04/05,mvzs-street,73,KS,KANSAS +579,eemhh-name,nsj-firstname,11660,buq-city,1986/06/23,mftt-street,191,NE,NEBRASKA +580,uyoei-name,wiz-firstname,19000,lqo-city,1974/06/18,rvat-street,197,NC,NORTH CAROLINA +581,ahbez-name,gwg-firstname,10940,gno-city,1978/03/23,rfwr-street,105,KS,KANSAS +582,qnopb-name,xgc-firstname,18800,ayx-city,1962/05/30,lrnc-street,193,TN,TENNESSEE +583,beqpz-name,psc-firstname,10880,sfe-city,1986/07/04,tobm-street,121,AK,ALASKA +584,nnvir-name,tbu-firstname,12860,cuj-city,1962/04/25,wuxg-street,11,WA,WASHINGTON +585,cmqrt-name,huk-firstname,19580,kae-city,1958/09/29,yukq-street,146,IA,IOWA +586,ifivh-name,bub-firstname,15640,pti-city,1950/04/21,alaw-street,104,VT,VERMONT +587,nqbqs-name,ndv-firstname,17500,sxr-city,1963/01/05,zclw-street,77,MP,NORTHERN MARIANA ISLANDS +588,kgrlf-name,suz-firstname,16080,ayf-city,1990/03/07,vmto-street,120,CO,COLORADO +589,nhjsx-name,xuh-firstname,12360,tqo-city,1970/08/04,vfzt-street,181,undefined,undefined +590,vdint-name,zal-firstname,14600,fjh-city,1982/04/22,uopg-street,117,WV,WEST VIRGINIA +591,nchqr-name,ldr-firstname,14700,pyy-city,1966/03/02,nlzm-street,137,VT,VERMONT +592,sjmbq-name,tsc-firstname,19960,dlp-city,1974/05/26,qphd-street,155,IN,INDIANA +593,njbsk-name,oft-firstname,17280,xtc-city,1958/02/13,sebl-street,85,HI,HAWAII +594,vtijb-name,obv-firstname,14000,bpn-city,1962/08/08,mxrl-street,199,IL,ILLINOIS +595,ihqjn-name,hnu-firstname,18960,ztv-city,1982/02/20,mhrl-street,45,MA,MASSACHUSETTS +596,kpzko-name,zfz-firstname,18720,rjc-city,1971/01/01,ycrf-street,173,CT,CONNECTICUT +597,qlfgm-name,fnv-firstname,11360,avw-city,1970/12/01,yvgg-street,49,LA,LOUISIANA +598,oeepm-name,asq-firstname,18660,mvq-city,1990/07/29,ssey-street,57,OH,OHIO +599,waklo-name,ksz-firstname,10660,ddf-city,1958/07/10,dilr-street,145,NY,NEW YORK +600,mdftt-name,ldq-firstname,11460,mkm-city,1966/09/03,rjkt-street,9,CA,CALIFORNIA +601,iowsr-name,vmg-firstname,18420,kie-city,1954/12/30,qfom-street,117,NE,NEBRASKA +602,sdtxj-name,zbp-firstname,16160,gic-city,1962/09/09,eavm-street,108,IA,IOWA +603,ocebd-name,img-firstname,12180,rxp-city,1966/08/18,ygmy-street,52,PA,PENNSYLVANIA +604,uvovz-name,nsd-firstname,13600,tlc-city,1950/08/13,xbsm-street,110,MP,NORTHERN MARIANA ISLANDS +605,fzuuf-name,pub-firstname,10500,ikg-city,1974/04/26,pmnl-street,189,MA,MASSACHUSETTS +606,xzvbs-name,qpb-firstname,17060,xed-city,1950/04/18,vvdx-street,118,OR,OREGON +607,qvkir-name,hns-firstname,12840,kxi-city,1978/03/02,kzbt-street,65,RI,RHODE ISLAND +608,llarb-name,psp-firstname,11240,wlq-city,1990/10/10,qshw-street,106,AL,ALABAMA +609,wrzts-name,ygm-firstname,15520,ybe-city,1974/12/03,uxct-street,200,ND,NORTH DAKOTA +610,qfpdp-name,luy-firstname,16260,qma-city,1978/07/12,opsm-street,194,RI,RHODE ISLAND +611,xaosj-name,kfd-firstname,18100,xmf-city,1982/12/22,hceh-street,75,LA,LOUISIANA +612,iuocr-name,ztn-firstname,19000,sby-city,1970/06/07,qres-street,86,TX,TEXAS +613,ocvrm-name,ylm-firstname,16360,vif-city,1986/11/17,zgov-street,17,CT,CONNECTICUT +614,tealr-name,lbu-firstname,10020,blg-city,1958/03/23,qjvy-street,182,MS,MISSISSIPPI +615,ksjja-name,dky-firstname,15360,qim-city,1986/08/30,rvtm-street,182,KY,KENTUCKY +616,jbhpa-name,shc-firstname,18020,gmc-city,1974/05/29,cvpl-street,154,AS,AMERICAN SAMOA +617,vraon-name,nam-firstname,16320,joa-city,1986/11/07,pnik-street,194,NM,NEW MEXICO +618,gtymv-name,lgo-firstname,16740,ynr-city,1970/05/14,xxzj-street,74,NJ,NEW JERSEY +619,sxbpo-name,hyx-firstname,12900,lvu-city,1958/07/14,tysz-street,68,HI,HAWAII +620,yaogj-name,fjg-firstname,11040,slr-city,1974/06/22,kjoi-street,134,OK,OKLAHOMA +621,rprrj-name,vca-firstname,19160,txr-city,1959/01/18,stjh-street,137,MA,MASSACHUSETTS +622,dbaaq-name,jdi-firstname,13800,uge-city,1954/11/08,ftck-street,175,OK,OKLAHOMA +623,eapsq-name,zeu-firstname,14720,mmh-city,1954/11/01,tjlj-street,84,AS,AMERICAN SAMOA +624,wlrqb-name,srf-firstname,15280,bbq-city,1970/04/06,thvb-street,17,CO,COLORADO +625,dzzia-name,ukn-firstname,14680,zug-city,1954/04/29,nthu-street,109,LA,LOUISIANA +626,nfzxa-name,ikc-firstname,17080,sqt-city,1982/01/21,dteu-street,113,CT,CONNECTICUT +627,ovxiz-name,lho-firstname,10620,gox-city,1954/07/05,cofd-street,155,NY,NEW YORK +628,msubu-name,ccj-firstname,17780,jkv-city,1982/02/25,gfve-street,43,NV,NEVADA +629,xxbdr-name,kas-firstname,17320,ick-city,1970/02/25,gvoi-street,24,ID,IDAHO +630,rgryb-name,guv-firstname,11160,xus-city,1966/05/06,plka-street,164,FL,FLORIDA +631,dxprz-name,byy-firstname,16520,lpb-city,1974/03/01,auoq-street,103,MH,MARSHALL ISLANDS +632,nerdb-name,eqa-firstname,12860,arg-city,1958/03/17,emcz-street,104,DC,DISTRICT OF COLUMBIA +633,nszhh-name,nrn-firstname,16860,hap-city,1974/04/30,zjuq-street,78,IA,IOWA +634,jtcxq-name,hxi-firstname,12160,oqq-city,1982/06/03,xpsp-street,30,VI,VIRGIN ISLANDS +635,prglc-name,kqd-firstname,16080,gqr-city,1978/10/14,pjin-street,145,NH,NEW HAMPSHIRE +636,yofsk-name,qch-firstname,14240,wbz-city,1990/05/28,edsh-street,5,MI,MICHIGAN +637,cupcm-name,jvy-firstname,13720,lkw-city,1966/10/14,frzx-street,83,NY,NEW YORK +638,crjjf-name,yfe-firstname,19880,oof-city,1970/02/07,akkj-street,98,NC,NORTH CAROLINA +639,rqyyg-name,hbx-firstname,12080,sxh-city,1958/04/24,wzjb-street,159,TX,TEXAS +640,xxugu-name,yqf-firstname,10380,ocx-city,1954/11/18,mbps-street,20,ND,NORTH DAKOTA +641,ayvhk-name,znx-firstname,16960,qbw-city,1954/07/23,wgsh-street,148,OK,OKLAHOMA +642,xwdxq-name,aik-firstname,14280,djc-city,1958/06/18,mjpa-street,130,OH,OHIO +643,qsvmf-name,lgn-firstname,17140,mpl-city,1982/06/15,ngot-street,193,NJ,NEW JERSEY +644,tlued-name,mcz-firstname,16780,oab-city,1978/09/22,ppzd-street,111,AR,ARKANSAS +645,ibhei-name,djd-firstname,11340,eep-city,1975/01/24,rzdt-street,94,MH,MARSHALL ISLANDS +646,snfrd-name,jof-firstname,12700,lck-city,1978/12/16,jpcu-street,184,IN,INDIANA +647,vkwed-name,elj-firstname,18940,tdi-city,1990/08/05,ekyx-street,137,KY,KENTUCKY +648,acing-name,rxv-firstname,10660,gtt-city,1970/11/17,ltxb-street,117,VA,VIRGINIA +649,fzmkk-name,ilb-firstname,18540,rwj-city,1986/02/08,xsnm-street,113,MA,MASSACHUSETTS +650,knzxd-name,seg-firstname,17180,oqu-city,1982/11/02,ukda-street,80,MT,MONTANA +651,zzyix-name,dlk-firstname,14180,yoc-city,1986/10/06,jnpv-street,71,FM,FEDERATED STATES OF MICRONESIA +652,nsote-name,ivj-firstname,19740,aqh-city,1990/11/05,faox-street,77,WI,WISCONSIN +653,znees-name,edg-firstname,18120,qkr-city,1986/12/08,prpo-street,42,DE,DELAWARE +654,svzbm-name,hsg-firstname,19180,shh-city,1954/02/05,uglh-street,150,IA,IOWA +655,llfur-name,geu-firstname,12340,qdf-city,1970/12/01,nntt-street,92,AZ,ARIZONA +656,pusrl-name,ovn-firstname,17020,oaa-city,1954/07/30,oxfp-street,104,AL,ALABAMA +657,wihqb-name,bkq-firstname,13540,jau-city,1974/09/25,vuga-street,83,NY,NEW YORK +658,ezgdt-name,yaa-firstname,13380,fdc-city,1986/11/04,yxut-street,75,VI,VIRGIN ISLANDS +659,kvtlv-name,zdr-firstname,15700,bqd-city,1990/06/04,kajd-street,92,DE,DELAWARE +660,bqkzl-name,tym-firstname,14900,rhk-city,1962/02/13,yyzk-street,165,MH,MARSHALL ISLANDS +661,ndfve-name,ajt-firstname,19460,tdr-city,1970/02/04,vpub-street,143,CT,CONNECTICUT +662,temdh-name,trc-firstname,11720,ald-city,1950/05/06,tapb-street,1,FL,FLORIDA +663,bzagi-name,dwa-firstname,13580,zvq-city,1970/05/05,khyi-street,105,CT,CONNECTICUT +664,frtvn-name,ukn-firstname,19400,eto-city,1986/06/29,dcnz-street,114,MH,MARSHALL ISLANDS +665,mjdqu-name,xlw-firstname,14420,lpe-city,1962/08/02,oecb-street,189,IN,INDIANA +666,fnopj-name,sdh-firstname,19080,hxm-city,1974/11/24,pfxz-street,66,ME,MAINE +667,kuxaz-name,hye-firstname,14860,eia-city,1962/09/16,nhdg-street,198,CO,COLORADO +668,tfrpe-name,mme-firstname,12860,mox-city,1978/03/24,uhqo-street,10,NE,NEBRASKA +669,eodve-name,tzj-firstname,11440,syb-city,1986/07/29,jlnw-street,74,WI,WISCONSIN +670,slzsi-name,nvw-firstname,13680,kgz-city,1958/07/13,pptx-street,88,NY,NEW YORK +671,wrttz-name,wwx-firstname,13440,lig-city,1986/11/05,vdph-street,66,MI,MICHIGAN +672,wfxhy-name,wtc-firstname,12760,gnf-city,1970/08/22,itut-street,3,HI,HAWAII +673,toeja-name,vrw-firstname,18900,xes-city,1982/09/01,wpqu-street,184,undefined,undefined +674,glmnt-name,bck-firstname,11680,cuu-city,1978/08/02,wtxv-street,16,RI,RHODE ISLAND +675,vyvnk-name,shl-firstname,10260,xhq-city,1970/06/06,ddbz-street,28,MA,MASSACHUSETTS +676,pjdvs-name,adv-firstname,15600,snf-city,1986/12/10,tdft-street,133,DC,DISTRICT OF COLUMBIA +677,rulrt-name,ytn-firstname,19340,spb-city,1974/11/11,bpwx-street,24,VA,VIRGINIA +678,gvttx-name,moj-firstname,17000,gri-city,1978/11/24,kqlt-street,27,ID,IDAHO +679,jtlah-name,gxq-firstname,15440,jte-city,1978/02/01,mbvv-street,172,OH,OHIO +680,cpvhs-name,ruo-firstname,16080,vhq-city,1970/10/18,ditw-street,164,AR,ARKANSAS +681,jikyx-name,hzl-firstname,13320,hgh-city,1958/08/07,gmqp-street,85,FL,FLORIDA +682,bsvsn-name,idb-firstname,15780,ooc-city,1958/04/28,apff-street,78,MO,MISSOURI +683,qgivn-name,bau-firstname,11580,uez-city,1987/01/29,vpsa-street,48,RI,RHODE ISLAND +684,hqbfl-name,fgr-firstname,11620,ejc-city,1982/02/07,fwsq-street,107,WA,WASHINGTON +685,crmtk-name,fmd-firstname,14480,mdp-city,1962/05/23,knxw-street,18,IL,ILLINOIS +686,kccgj-name,pzd-firstname,10760,ifv-city,1990/05/14,dgfl-street,172,PR,PUERTO RICO +687,mddbw-name,lcu-firstname,11360,crz-city,1970/08/24,eiwf-street,32,PR,PUERTO RICO +688,dlnxj-name,tzx-firstname,11380,nza-city,1966/09/25,jlna-street,186,MO,MISSOURI +689,wpjcx-name,hwx-firstname,13100,slr-city,1987/01/16,wxcr-street,42,AK,ALASKA +690,yefnh-name,jzp-firstname,13080,ozf-city,1986/08/09,zzos-street,120,OR,OREGON +691,qsoei-name,kio-firstname,11860,esa-city,1974/08/04,bhbl-street,86,HI,HAWAII +692,xwodh-name,aid-firstname,17940,wgd-city,1954/07/21,rcrf-street,90,IL,ILLINOIS +693,oncpb-name,elk-firstname,12020,rbp-city,1974/10/28,iblz-street,114,DE,DELAWARE +694,qisar-name,kfc-firstname,19800,alw-city,1958/08/07,ukbj-street,53,NH,NEW HAMPSHIRE +695,bvsga-name,qln-firstname,18060,zlt-city,1966/01/29,rngf-street,46,WA,WASHINGTON +696,xguku-name,uxf-firstname,10080,ezg-city,1974/12/30,icvq-street,48,UT,UTAH +697,hxylt-name,hng-firstname,15360,jgc-city,1966/03/11,lhxd-street,87,DE,DELAWARE +698,dmlyv-name,qai-firstname,20000,hoe-city,1966/08/20,uboj-street,72,KY,KENTUCKY +699,kkbjx-name,cnx-firstname,16780,ndu-city,1970/08/16,afzw-street,55,NJ,NEW JERSEY +700,emoio-name,awr-firstname,18560,qpj-city,1974/11/17,hasx-street,92,VT,VERMONT +701,lxpni-name,oox-firstname,14100,xrj-city,1986/06/09,wvvi-street,106,MO,MISSOURI +702,oshuu-name,mim-firstname,14280,mns-city,1982/09/12,ashj-street,95,FM,FEDERATED STATES OF MICRONESIA +703,uhpxg-name,gnt-firstname,19180,zsi-city,1978/05/22,namc-street,126,AL,ALABAMA +704,lvoek-name,reg-firstname,18920,ubj-city,1954/12/24,nrjw-street,38,PA,PENNSYLVANIA +705,lpnhx-name,szp-firstname,10720,ybd-city,1970/11/26,ntyc-street,109,GA,GEORGIA +706,kppvg-name,ztz-firstname,14660,ocp-city,1990/08/19,ekcr-street,56,SD,SOUTH DAKOTA +707,vxwxv-name,qzo-firstname,15540,rfl-city,1962/03/29,hcwy-street,116,KY,KENTUCKY +708,nbgan-name,ocf-firstname,15240,aas-city,1954/09/14,zkkh-street,117,IN,INDIANA +709,faiaw-name,lup-firstname,16980,rsz-city,1986/09/17,yddi-street,98,IN,INDIANA +710,mdmax-name,ggz-firstname,17360,stn-city,1966/11/25,lwhf-street,198,IN,INDIANA +711,etauo-name,pta-firstname,13360,fnh-city,1978/02/03,gsrn-street,113,IA,IOWA +712,gckpz-name,rou-firstname,17920,liu-city,1974/10/10,apjm-street,40,AZ,ARIZONA +713,szvhz-name,ani-firstname,17240,nab-city,1958/12/10,cxho-street,87,KY,KENTUCKY +714,dawkl-name,epe-firstname,11780,pjg-city,1990/04/24,hdbx-street,104,WI,WISCONSIN +715,eifes-name,zka-firstname,11500,uux-city,1986/10/20,pazo-street,125,TN,TENNESSEE +716,fphzw-name,gof-firstname,14680,dkz-city,1986/06/23,aejx-street,88,VA,VIRGINIA +717,bgicm-name,one-firstname,14820,lwh-city,1966/01/08,dizm-street,53,AR,ARKANSAS +718,qnozr-name,fwa-firstname,17800,rsm-city,1954/04/11,tuun-street,63,NE,NEBRASKA +719,zxcjb-name,mhs-firstname,18620,czu-city,1982/11/15,ucfg-street,103,PW,PALAU +720,qqftw-name,dox-firstname,11280,eii-city,1982/08/03,akjj-street,174,VT,VERMONT +721,byevj-name,bah-firstname,14240,rge-city,1958/01/21,rkkm-street,142,MD,MARYLAND +722,ihvvb-name,gaq-firstname,19140,gua-city,1958/09/30,hxim-street,88,RI,RHODE ISLAND +723,suiee-name,vji-firstname,12120,zuz-city,1970/02/22,ijcn-street,84,PW,PALAU +724,fgkoi-name,xkx-firstname,15880,zuk-city,1970/12/27,frfg-street,74,IL,ILLINOIS +725,aowhf-name,hvv-firstname,12740,mpj-city,1987/01/06,mrmx-street,95,KY,KENTUCKY +726,vxkbl-name,xfk-firstname,18600,odo-city,1970/02/28,jrzc-street,128,AZ,ARIZONA +727,zpedp-name,oxh-firstname,16380,cjz-city,1958/05/28,bnxj-street,18,GU,GUAM +728,uqfju-name,dmq-firstname,15560,ehf-city,1954/03/02,ptqn-street,68,VA,VIRGINIA +729,wsoxm-name,xtd-firstname,15660,ogk-city,1951/01/29,inkh-street,150,WV,WEST VIRGINIA +730,cvypk-name,xoy-firstname,15080,cwr-city,1970/02/28,kyio-street,117,WV,WEST VIRGINIA +731,yprgp-name,vgc-firstname,14660,alm-city,1971/01/05,uoxp-street,110,CA,CALIFORNIA +732,udhil-name,dsa-firstname,12060,lzv-city,1974/02/18,gvvc-street,149,AK,ALASKA +733,fgtjr-name,bnt-firstname,18240,pgf-city,1986/10/15,synp-street,159,CT,CONNECTICUT +734,hypgs-name,ycz-firstname,10840,ufx-city,1966/06/26,frur-street,81,HI,HAWAII +735,ijqas-name,bgl-firstname,18680,dss-city,1950/02/20,rafi-street,134,ID,IDAHO +736,seeae-name,pvg-firstname,10800,txd-city,1958/10/18,uibx-street,102,GU,GUAM +737,punzk-name,ubx-firstname,15020,qzq-city,1978/07/13,cqww-street,52,PA,PENNSYLVANIA +738,xyvjy-name,jta-firstname,19700,koe-city,1986/06/22,lrjw-street,17,undefined,undefined +739,ysnek-name,kop-firstname,13120,bel-city,1950/08/01,xcbk-street,94,MA,MASSACHUSETTS +740,ozxng-name,sje-firstname,15180,nqf-city,1986/11/19,uivr-street,117,MP,NORTHERN MARIANA ISLANDS +741,ekqcg-name,hxq-firstname,16880,jic-city,1966/01/11,grbw-street,86,UT,UTAH +742,xxhts-name,krw-firstname,14140,lwz-city,1978/04/06,fzbe-street,40,WI,WISCONSIN +743,uffha-name,jdu-firstname,13080,jmd-city,1986/06/21,sqbk-street,66,KS,KANSAS +744,aevbo-name,bba-firstname,16880,hgh-city,1958/03/08,urgb-street,74,NE,NEBRASKA +745,rqwnb-name,zpb-firstname,19300,cbx-city,1978/08/12,qavq-street,5,IN,INDIANA +746,txozw-name,vet-firstname,15540,ajt-city,1954/07/05,jayh-street,105,RI,RHODE ISLAND +747,xmyzv-name,phl-firstname,15220,jnk-city,1974/04/04,ckwm-street,174,KY,KENTUCKY +748,fusnk-name,iva-firstname,14660,kqd-city,1982/07/03,zmbq-street,10,FL,FLORIDA +749,ksnje-name,eaq-firstname,12500,gej-city,1982/10/03,vbwg-street,83,HI,HAWAII +750,kcahb-name,srs-firstname,11300,bvz-city,1978/05/16,gyfr-street,164,LA,LOUISIANA +751,bzjht-name,vfn-firstname,17500,mgi-city,1958/06/15,mmko-street,172,DE,DELAWARE +752,loydq-name,gft-firstname,11900,fsr-city,1990/01/20,sjds-street,62,TN,TENNESSEE +753,kglxv-name,zlt-firstname,14960,qgb-city,1982/02/07,gniz-street,178,AS,AMERICAN SAMOA +754,loimm-name,ipg-firstname,15980,sxt-city,1978/10/31,kdlk-street,22,NY,NEW YORK +755,demtf-name,yhq-firstname,12300,pdo-city,1982/09/18,mfye-street,99,TN,TENNESSEE +756,jdmdi-name,llu-firstname,18060,pss-city,1974/07/01,utrs-street,198,CA,CALIFORNIA +757,crjfv-name,ccn-firstname,19520,uub-city,1978/07/12,xtvk-street,138,OK,OKLAHOMA +758,irghm-name,dbi-firstname,15200,otd-city,1982/12/14,iple-street,144,WY,WYOMING +759,mcwlu-name,kxy-firstname,17640,edu-city,1958/07/05,ibtg-street,173,IN,INDIANA +760,plfvg-name,cxa-firstname,19360,rnc-city,1958/04/06,zemc-street,64,DE,DELAWARE +761,dnxai-name,gnd-firstname,11580,ers-city,1966/07/14,ytjt-street,163,OH,OHIO +762,ksbwq-name,tku-firstname,10040,axu-city,1974/08/10,kvel-street,95,RI,RHODE ISLAND +763,durgp-name,unr-firstname,12820,juk-city,1959/01/05,ucky-street,132,HI,HAWAII +764,eocqj-name,qcg-firstname,18940,faf-city,1986/10/14,gjjy-street,126,OK,OKLAHOMA +765,dhcim-name,eti-firstname,17140,aed-city,1982/07/19,syxk-street,156,FL,FLORIDA +766,nerhu-name,jfx-firstname,10140,huk-city,1978/02/07,cyfs-street,47,ME,MAINE +767,sxukr-name,hag-firstname,14400,lfc-city,1963/01/05,wylb-street,38,WV,WEST VIRGINIA +768,xznte-name,snc-firstname,17840,mhl-city,1962/03/23,rfig-street,151,DE,DELAWARE +769,tgwig-name,rfn-firstname,14020,fcs-city,1986/11/12,agpo-street,79,undefined,undefined +770,kmkbd-name,fzv-firstname,12920,kjt-city,1974/03/03,sgjo-street,63,MN,MINNESOTA +771,kqzne-name,gvz-firstname,15500,zar-city,1966/04/01,kvlk-street,35,DC,DISTRICT OF COLUMBIA +772,hatuj-name,fwt-firstname,16040,gih-city,1978/07/10,wbjj-street,70,MD,MARYLAND +773,anvjr-name,qqu-firstname,15280,php-city,1970/09/28,hhnz-street,147,GU,GUAM +774,jxiox-name,ein-firstname,18560,vou-city,1974/04/29,dlcm-street,52,VA,VIRGINIA +775,ahdci-name,fhq-firstname,10040,dis-city,1974/05/17,flyu-street,84,MO,MISSOURI +776,slvkr-name,qtq-firstname,10800,seu-city,1978/08/24,hivm-street,148,PR,PUERTO RICO +777,xncyt-name,alj-firstname,12500,qcq-city,1962/05/20,bqjo-street,78,VI,VIRGIN ISLANDS +778,oagkd-name,oud-firstname,10720,nai-city,1974/09/12,jbps-street,50,MT,MONTANA +779,bhinx-name,sde-firstname,18240,zrt-city,1962/07/08,dxuq-street,97,PA,PENNSYLVANIA +780,wpyvx-name,qwg-firstname,18520,lku-city,1966/03/09,ymkt-street,133,MD,MARYLAND +781,wvkan-name,ndd-firstname,10340,hwn-city,1978/11/18,pqqf-street,114,NV,NEVADA +782,biapu-name,kiq-firstname,16360,lbi-city,1954/10/27,pawe-street,198,OK,OKLAHOMA +783,ohygc-name,vxw-firstname,11100,flm-city,1966/11/20,homq-street,170,HI,HAWAII +784,ntbgw-name,msg-firstname,13740,cth-city,1958/09/28,epdz-street,80,AS,AMERICAN SAMOA +785,vegli-name,avd-firstname,18000,gob-city,1970/08/27,bvnt-street,32,WY,WYOMING +786,aadoy-name,gre-firstname,16040,qln-city,1962/01/03,lqxs-street,179,ME,MAINE +787,rzwid-name,xjl-firstname,16300,eds-city,1970/05/08,hutz-street,27,TX,TEXAS +788,qperl-name,nsk-firstname,13920,hxo-city,1966/11/16,xvzr-street,160,IA,IOWA +789,hkhzc-name,hxd-firstname,19980,gpm-city,1954/08/23,qmrm-street,60,VA,VIRGINIA +790,qlixr-name,tsd-firstname,16780,dvv-city,1978/02/14,ibwa-street,85,AZ,ARIZONA +791,ieyhf-name,oox-firstname,19820,mfp-city,1958/02/17,ntdb-street,100,MD,MARYLAND +792,xxtwm-name,zpp-firstname,12980,udl-city,1970/03/01,dkzb-street,157,DC,DISTRICT OF COLUMBIA +793,hvtaa-name,wcr-firstname,14460,zek-city,1970/01/18,bayt-street,149,PR,PUERTO RICO +794,xduae-name,iqk-firstname,18380,lkm-city,1982/11/24,aujb-street,62,GA,GEORGIA +795,qkjcz-name,abw-firstname,15080,fco-city,1986/11/01,tkuc-street,69,FM,FEDERATED STATES OF MICRONESIA +796,jplol-name,akr-firstname,19760,rvj-city,1970/12/23,sqxe-street,128,SD,SOUTH DAKOTA +797,iegsb-name,ujo-firstname,18480,abx-city,1974/02/24,esda-street,16,RI,RHODE ISLAND +798,ypqix-name,hbp-firstname,13240,kdv-city,1986/05/20,iyaq-street,10,OH,OHIO +799,awluy-name,ysq-firstname,13680,kcv-city,1986/12/16,rtbk-street,144,ID,IDAHO +800,vtyuq-name,cup-firstname,13200,gcq-city,1970/11/05,omwn-street,156,AR,ARKANSAS +801,hmvzq-name,jgx-firstname,18660,hbj-city,1982/03/30,hxik-street,59,MO,MISSOURI +802,vhatl-name,ywe-firstname,10040,ytj-city,1982/02/10,lblz-street,62,CT,CONNECTICUT +803,pvozw-name,pln-firstname,13980,tdu-city,1986/06/18,ulqg-street,8,MT,MONTANA +804,mwmvk-name,ttp-firstname,14020,dcw-city,1982/08/26,lqrt-street,104,CO,COLORADO +805,pmlsn-name,uxe-firstname,17680,udv-city,1962/12/27,usnv-street,155,PR,PUERTO RICO +806,uuvrf-name,jxu-firstname,16360,vpo-city,1978/08/04,tpez-street,25,MS,MISSISSIPPI +807,pvcck-name,nnx-firstname,19780,kfm-city,1962/07/05,aybu-street,151,CA,CALIFORNIA +808,kdybn-name,qcg-firstname,11080,wno-city,1978/05/06,omzx-street,56,GU,GUAM +809,ylapy-name,mou-firstname,11400,yky-city,1990/03/12,rcbx-street,167,WI,WISCONSIN +810,ygvfd-name,hqp-firstname,13240,sfw-city,1958/05/16,svut-street,16,RI,RHODE ISLAND +811,wgbsf-name,wgj-firstname,12000,itm-city,1966/07/31,xlwg-street,122,NV,NEVADA +812,guhzx-name,hde-firstname,19260,kqz-city,1962/03/01,escf-street,84,NY,NEW YORK +813,quyjf-name,vta-firstname,14440,yiw-city,1978/01/29,nsss-street,83,ND,NORTH DAKOTA +814,fsbhc-name,ink-firstname,19220,qjo-city,1974/08/30,pwtn-street,140,WA,WASHINGTON +815,zhzwu-name,pkl-firstname,17680,slh-city,1966/06/13,cspv-street,106,NJ,NEW JERSEY +816,sqegy-name,ufg-firstname,19220,hry-city,1954/03/25,anrd-street,173,PW,PALAU +817,bkiln-name,sjs-firstname,15100,fao-city,1958/09/04,inav-street,170,AR,ARKANSAS +818,zynej-name,zgh-firstname,15100,spc-city,1962/01/25,zewf-street,85,GU,GUAM +819,xzasj-name,kib-firstname,18080,rzn-city,1970/10/07,tlsi-street,190,SD,SOUTH DAKOTA +820,tmqlc-name,jes-firstname,11560,vhc-city,1974/11/27,xxkh-street,36,MO,MISSOURI +821,gowim-name,rcr-firstname,11200,kja-city,1974/06/06,zhkg-street,41,MO,MISSOURI +822,bulti-name,cse-firstname,13660,ukw-city,1970/12/14,glrc-street,59,NV,NEVADA +823,evxmh-name,gfc-firstname,13560,cbo-city,1986/03/05,sfkz-street,72,ID,IDAHO +824,dbdop-name,swu-firstname,14760,tlv-city,1982/10/06,vprj-street,87,AR,ARKANSAS +825,mgyeq-name,wko-firstname,15320,lxz-city,1990/03/22,mfwa-street,11,DE,DELAWARE +826,bfplt-name,ard-firstname,10360,sou-city,1990/02/13,jniu-street,124,NC,NORTH CAROLINA +827,dvbot-name,auq-firstname,16200,qkn-city,1978/03/20,qfzt-street,9,IL,ILLINOIS +828,tncjm-name,kvn-firstname,11640,mlf-city,1954/02/08,igos-street,166,ID,IDAHO +829,svoxr-name,rbc-firstname,10400,bny-city,1958/07/13,socq-street,198,MH,MARSHALL ISLANDS +830,jxzxn-name,wau-firstname,14360,orx-city,1958/07/08,fssh-street,77,NM,NEW MEXICO +831,dglke-name,dyc-firstname,13100,lgz-city,1982/12/18,qixe-street,158,MH,MARSHALL ISLANDS +832,pewvm-name,asq-firstname,10840,czn-city,1966/04/02,wnac-street,84,MD,MARYLAND +833,tfgsc-name,vxn-firstname,10100,wup-city,1982/02/05,qlsb-street,193,AK,ALASKA +834,hmoxn-name,vti-firstname,13660,gvq-city,1974/11/25,dahy-street,123,HI,HAWAII +835,bexku-name,mvy-firstname,13080,wrx-city,1966/09/25,ovag-street,86,GA,GEORGIA +836,ksffn-name,ubg-firstname,18600,gsu-city,1958/02/03,vepx-street,58,NE,NEBRASKA +837,ngfsu-name,pgq-firstname,10400,pag-city,1986/06/19,rwqz-street,123,DE,DELAWARE +838,mopuh-name,vdg-firstname,14160,xbe-city,1970/11/22,vbak-street,103,MA,MASSACHUSETTS +839,rifdh-name,eky-firstname,12900,uwr-city,1978/04/02,yysd-street,81,GU,GUAM +840,avlwf-name,xpn-firstname,14940,ebu-city,1974/08/06,vcki-street,154,PW,PALAU +841,avokq-name,dzw-firstname,14680,xnx-city,1954/02/23,cmis-street,175,NH,NEW HAMPSHIRE +842,nahim-name,yvv-firstname,18020,egx-city,1978/05/05,rpeq-street,86,NJ,NEW JERSEY +843,kehud-name,gus-firstname,13200,bwc-city,1962/02/19,aagh-street,166,OK,OKLAHOMA +844,tkfjt-name,kch-firstname,11160,vuw-city,1974/07/20,mkli-street,119,WY,WYOMING +845,gcwwn-name,rpb-firstname,10640,yum-city,1982/05/17,vqgz-street,157,OR,OREGON +846,piobc-name,pii-firstname,17360,vcs-city,1950/06/02,obsu-street,105,FM,FEDERATED STATES OF MICRONESIA +847,nfguz-name,rtb-firstname,14380,zrh-city,1982/03/01,jltk-street,164,PW,PALAU +848,kayxc-name,gew-firstname,15780,miz-city,1959/01/22,bckb-street,85,ME,MAINE +849,kwjrg-name,lwy-firstname,19080,qym-city,1986/08/26,fagw-street,181,OR,OREGON +850,joacl-name,nay-firstname,15120,wdm-city,1982/12/10,goug-street,6,VT,VERMONT +851,fmnme-name,ctx-firstname,13940,qnj-city,1954/04/12,cacp-street,155,AL,ALABAMA +852,cqjtz-name,vqp-firstname,16480,san-city,1966/10/16,oijg-street,94,NY,NEW YORK +853,tdndt-name,qya-firstname,16320,log-city,1962/05/08,pvtk-street,151,MP,NORTHERN MARIANA ISLANDS +854,cquwe-name,lzs-firstname,12940,vnf-city,1986/08/16,tsfg-street,181,SD,SOUTH DAKOTA +855,dbfxb-name,glb-firstname,18420,ecz-city,1978/11/28,fpuk-street,42,PW,PALAU +856,odwcc-name,tcz-firstname,17420,ffa-city,1954/06/12,kirb-street,158,WI,WISCONSIN +857,qctkr-name,wpo-firstname,16980,upo-city,1978/06/21,gfrm-street,116,TX,TEXAS +858,qikks-name,eep-firstname,18560,tof-city,1986/08/11,trqf-street,64,WA,WASHINGTON +859,gbltk-name,jny-firstname,12080,tdu-city,1978/09/15,wsii-street,133,ID,IDAHO +860,cirdu-name,dzw-firstname,11340,stm-city,1974/04/04,qnat-street,48,PA,PENNSYLVANIA +861,xukav-name,rep-firstname,18240,pzu-city,1950/11/16,piru-street,24,PW,PALAU +862,dqxqx-name,eca-firstname,16080,ddl-city,1982/08/15,snln-street,109,AK,ALASKA +863,jxcxv-name,ipw-firstname,19200,ctv-city,1982/12/03,zazz-street,171,TN,TENNESSEE +864,ddgxr-name,bwx-firstname,18860,giq-city,1954/05/11,ukyp-street,161,SC,SOUTH CAROLINA +865,wcboj-name,cqb-firstname,15080,srk-city,1978/04/28,pycv-street,12,PW,PALAU +866,ifgzt-name,bde-firstname,17700,whw-city,1978/05/05,qfgv-street,162,SD,SOUTH DAKOTA +867,riwqf-name,qil-firstname,18480,mce-city,1990/09/07,qxyc-street,69,MP,NORTHERN MARIANA ISLANDS +868,zdudw-name,yxg-firstname,17380,apv-city,1954/09/09,wymd-street,195,KS,KANSAS +869,szvfk-name,nei-firstname,13660,xqq-city,1974/10/14,ygbr-street,129,KY,KENTUCKY +870,lrgna-name,lqv-firstname,16840,woh-city,1978/09/16,ibzb-street,87,IL,ILLINOIS +871,jeaqx-name,kdj-firstname,16840,lpn-city,1979/01/26,guxb-street,10,WA,WASHINGTON +872,ubvth-name,njr-firstname,14920,hbz-city,1966/10/20,knnm-street,69,AL,ALABAMA +873,opkuq-name,sbc-firstname,10520,wpu-city,1974/10/24,tzoa-street,10,OK,OKLAHOMA +874,eeesy-name,sei-firstname,13420,xhc-city,1978/01/10,wsmi-street,48,AK,ALASKA +875,njzrw-name,yep-firstname,13680,hdj-city,1974/05/12,bhba-street,42,ME,MAINE +876,caria-name,jaf-firstname,18660,scx-city,1962/12/18,envv-street,158,GU,GUAM +877,sdwui-name,dax-firstname,17780,mdr-city,1958/12/22,orah-street,167,AK,ALASKA +878,pnklv-name,not-firstname,11160,gfk-city,1982/08/07,muqk-street,6,AZ,ARIZONA +879,xhqwk-name,cgz-firstname,19940,nji-city,1962/05/06,ihqj-street,97,TN,TENNESSEE +880,oigoy-name,ccp-firstname,14060,ase-city,1958/05/20,faef-street,133,MD,MARYLAND +881,nysrz-name,nil-firstname,17700,ile-city,1966/09/13,aqiy-street,25,WI,WISCONSIN +882,wrkvm-name,ftf-firstname,17780,wim-city,1982/12/12,kzed-street,57,MN,MINNESOTA +883,zngvz-name,tcp-firstname,11640,vba-city,1982/05/29,phle-street,105,OK,OKLAHOMA +884,wdubl-name,his-firstname,19840,ybx-city,1978/08/06,upqg-street,80,TN,TENNESSEE +885,fxvpc-name,yxz-firstname,13380,ocv-city,1958/04/08,shtt-street,28,IL,ILLINOIS +886,blbvw-name,nln-firstname,17740,gwm-city,1955/01/28,ratd-street,98,UT,UTAH +887,ajxnx-name,utp-firstname,15540,eak-city,1950/07/01,zlzg-street,74,OR,OREGON +888,aeeid-name,bmc-firstname,19100,qtd-city,1958/10/28,sgwc-street,127,OH,OHIO +889,ypwhc-name,ojx-firstname,11480,zib-city,1967/01/09,gpfp-street,76,UT,UTAH +890,kuxdp-name,exg-firstname,14840,cvh-city,1986/01/22,jbif-street,108,TN,TENNESSEE +891,ujwmw-name,aqz-firstname,14080,mwz-city,1978/07/22,eggm-street,8,MN,MINNESOTA +892,xogup-name,pps-firstname,11740,bic-city,1970/11/26,thtu-street,34,ME,MAINE +893,vmrty-name,rmg-firstname,11060,cna-city,1970/08/27,uknc-street,24,GA,GEORGIA +894,rrwco-name,dji-firstname,18380,oln-city,1974/01/02,fzqy-street,129,MI,MICHIGAN +895,dvcbl-name,nam-firstname,15040,sbp-city,1962/03/02,txma-street,170,HI,HAWAII +896,tbsrx-name,krm-firstname,15220,gao-city,1959/01/03,fmhf-street,125,TX,TEXAS +897,ymjve-name,nmc-firstname,19280,yux-city,1974/12/11,ycqn-street,65,FL,FLORIDA +898,rycyz-name,prd-firstname,15920,oml-city,1982/09/13,ivie-street,30,OK,OKLAHOMA +899,agxhw-name,zla-firstname,14780,pkw-city,1970/04/28,mcrt-street,139,MN,MINNESOTA +900,bbdii-name,wkd-firstname,15220,eup-city,1962/05/01,neqn-street,191,ID,IDAHO +901,svszz-name,gyt-firstname,12900,sav-city,1954/09/30,gqnm-street,117,SC,SOUTH CAROLINA +902,dqidv-name,ooi-firstname,17340,krq-city,1978/06/24,hjtm-street,106,NJ,NEW JERSEY +903,blcfn-name,lrz-firstname,19240,sav-city,1978/03/01,xrfr-street,8,RI,RHODE ISLAND +904,acgjp-name,ncp-firstname,17900,snk-city,1974/10/27,qbrs-street,65,IA,IOWA +905,eloeo-name,tmh-firstname,17500,use-city,1971/01/05,hkrt-street,23,PA,PENNSYLVANIA +906,lxogw-name,hlp-firstname,10960,yhi-city,1990/10/25,ewnk-street,7,GU,GUAM +907,gvwtl-name,dmy-firstname,19920,fqc-city,1970/05/02,mpze-street,63,LA,LOUISIANA +908,vrrxx-name,xmg-firstname,17160,yks-city,1978/08/14,hikr-street,165,PW,PALAU +909,hkpuu-name,ofx-firstname,17040,hid-city,1978/02/03,rkrn-street,8,UT,UTAH +910,bupib-name,ftw-firstname,16620,fsw-city,1971/01/09,uzxm-street,95,RI,RHODE ISLAND +911,siwci-name,fir-firstname,11180,sqp-city,1974/06/27,pgeq-street,134,AS,AMERICAN SAMOA +912,mpkrp-name,etl-firstname,10560,kty-city,1958/06/02,cpug-street,67,CA,CALIFORNIA +913,itfnp-name,ncv-firstname,16440,dgw-city,1958/07/29,xdkl-street,131,OK,OKLAHOMA +914,oyvqm-name,lwu-firstname,14760,dzo-city,1958/07/09,ubqh-street,57,IA,IOWA +915,kuila-name,umo-firstname,11880,rhy-city,1982/09/17,jwha-street,81,AK,ALASKA +916,zduzo-name,gmh-firstname,12360,oxv-city,1990/06/26,dbxf-street,76,VI,VIRGIN ISLANDS +917,cxggf-name,fld-firstname,12900,sif-city,1954/09/08,wdis-street,142,WI,WISCONSIN +918,euelo-name,sbi-firstname,19340,shq-city,1986/04/20,qmja-street,3,GA,GEORGIA +919,papgo-name,oyu-firstname,10280,bet-city,1970/06/19,otzt-street,155,CA,CALIFORNIA +920,dtjeu-name,gvg-firstname,11980,aie-city,1954/10/17,tcpt-street,77,NY,NEW YORK +921,xryzl-name,oqb-firstname,16540,era-city,1962/08/27,aywk-street,118,VI,VIRGIN ISLANDS +922,pydqj-name,rwh-firstname,15640,ekn-city,1978/09/16,yecx-street,30,WA,WASHINGTON +923,picbf-name,cru-firstname,16040,iaa-city,1986/07/01,ttsg-street,112,MO,MISSOURI +924,jfqgk-name,ghq-firstname,10420,yap-city,1958/06/19,qzbd-street,1,NV,NEVADA +925,iqoex-name,dha-firstname,17340,fqq-city,1966/05/09,mbkz-street,151,VT,VERMONT +926,xlugu-name,fae-firstname,12060,uvm-city,1954/11/21,dhin-street,100,CT,CONNECTICUT +927,pxbfc-name,cia-firstname,15160,xrr-city,1986/06/20,psch-street,122,MT,MONTANA +928,koqls-name,teq-firstname,16100,vtl-city,1982/04/28,qfxy-street,11,ME,MAINE +929,nrhco-name,cwj-firstname,17380,zzw-city,1958/06/19,dcir-street,188,WA,WASHINGTON +930,rrxbl-name,gfc-firstname,13160,ruo-city,1982/06/02,yers-street,108,MH,MARSHALL ISLANDS +931,rqtri-name,oqi-firstname,19780,jvk-city,1970/12/08,dfnt-street,196,IA,IOWA +932,txjky-name,gvy-firstname,19040,jwu-city,1982/03/01,pacb-street,83,UT,UTAH +933,efrbq-name,tgu-firstname,13200,xgk-city,1970/04/10,ipjk-street,156,WI,WISCONSIN +934,xzrxi-name,lwt-firstname,16980,zll-city,1982/01/27,geic-street,42,LA,LOUISIANA +935,uafzk-name,wox-firstname,10240,agj-city,1978/07/03,wdwx-street,13,TX,TEXAS +936,npomk-name,upa-firstname,19060,oad-city,1986/06/27,qkua-street,200,MP,NORTHERN MARIANA ISLANDS +937,bepdw-name,box-firstname,13280,sxr-city,1978/04/15,rnub-street,19,ME,MAINE +938,qbbcf-name,igs-firstname,15440,xrs-city,1958/09/13,yhum-street,176,MA,MASSACHUSETTS +939,hirau-name,vyi-firstname,15980,iyv-city,1962/09/17,gqqy-street,48,FM,FEDERATED STATES OF MICRONESIA +940,fzpiq-name,rco-firstname,10660,wpc-city,1978/03/30,kktg-street,149,NY,NEW YORK +941,kvtgo-name,wkl-firstname,13840,yik-city,1962/02/18,goyb-street,15,NV,NEVADA +942,mpljt-name,xyx-firstname,14880,kdx-city,1982/05/25,fkgb-street,50,WV,WEST VIRGINIA +943,cxjtz-name,zkx-firstname,18500,mrg-city,1974/09/04,seup-street,139,MP,NORTHERN MARIANA ISLANDS +944,cbkvb-name,uji-firstname,13260,rtg-city,1954/02/08,oedv-street,5,AZ,ARIZONA +945,qsujg-name,hig-firstname,11980,obk-city,1974/12/24,awap-street,140,WI,WISCONSIN +946,faqty-name,wml-firstname,19180,qmg-city,1958/07/06,zxlq-street,9,NV,NEVADA +947,cdwge-name,aeu-firstname,16940,axh-city,1970/12/16,nkhw-street,165,RI,RHODE ISLAND +948,ctmij-name,tqq-firstname,12600,apv-city,1990/12/10,egyw-street,94,MD,MARYLAND +949,axcvk-name,dhn-firstname,11900,osf-city,1982/03/17,tzwx-street,71,OH,OHIO +950,flhxd-name,tty-firstname,10380,nfn-city,1970/04/16,iweq-street,144,MN,MINNESOTA +951,bnnjj-name,ttq-firstname,11360,shi-city,1966/11/16,xhde-street,125,MI,MICHIGAN +952,ohowr-name,rlf-firstname,16380,dzs-city,1982/05/12,yiwv-street,46,IL,ILLINOIS +953,ofqhs-name,pyy-firstname,12100,pfy-city,1958/04/28,xqtd-street,148,UT,UTAH +954,quyhm-name,oln-firstname,12140,lqe-city,1978/09/28,prlu-street,151,MO,MISSOURI +955,ailpr-name,cut-firstname,13200,yqu-city,1950/07/05,fxsk-street,101,IA,IOWA +956,zeyok-name,vhb-firstname,17240,nih-city,1986/11/30,bvms-street,86,WA,WASHINGTON +957,xrfau-name,owh-firstname,17180,tep-city,1954/03/02,qcmo-street,45,NJ,NEW JERSEY +958,nuoiw-name,rxs-firstname,11320,uef-city,1962/04/23,oiuf-street,117,MA,MASSACHUSETTS +959,imjdp-name,mlh-firstname,19020,mpi-city,1962/06/28,obxd-street,11,PA,PENNSYLVANIA +960,ynmgr-name,uzp-firstname,15500,oxs-city,1986/07/15,epkx-street,92,GA,GEORGIA +961,wcucp-name,qlt-firstname,12460,sow-city,1986/05/17,qoxd-street,40,WV,WEST VIRGINIA +962,ugiex-name,ebp-firstname,15640,vgo-city,1974/09/28,ggdc-street,22,MH,MARSHALL ISLANDS +963,nsilq-name,vxf-firstname,19280,zzm-city,1986/03/13,kxrw-street,68,VA,VIRGINIA +964,whtok-name,oqs-firstname,18340,ddf-city,1954/10/13,toaz-street,100,VT,VERMONT +965,lbwzb-name,rgg-firstname,15340,bla-city,1978/12/10,usrd-street,175,TN,TENNESSEE +966,lcbat-name,nnd-firstname,16200,ebl-city,1990/11/20,emud-street,144,MI,MICHIGAN +967,alouv-name,omr-firstname,14980,ouw-city,1958/06/11,xbnb-street,90,AZ,ARIZONA +968,nvqwv-name,blm-firstname,17880,nip-city,1974/12/14,sgzi-street,33,AS,AMERICAN SAMOA +969,wdidq-name,qwd-firstname,15480,eis-city,1979/01/22,qqbv-street,43,NH,NEW HAMPSHIRE +970,jmlfw-name,gal-firstname,12680,fjt-city,1955/01/10,iqnm-street,81,GU,GUAM +971,feoxv-name,mme-firstname,18020,oed-city,1986/01/13,jtud-street,73,NV,NEVADA +972,gsdoz-name,oxc-firstname,10420,jff-city,1978/02/20,jnom-street,62,OR,OREGON +973,hklot-name,rur-firstname,13900,nur-city,1986/06/05,nkmx-street,195,SD,SOUTH DAKOTA +974,smemp-name,ehc-firstname,10080,wrv-city,1950/04/10,wfrv-street,93,FL,FLORIDA +975,htgft-name,nzy-firstname,16960,gwe-city,1966/05/09,abiz-street,58,UT,UTAH +976,jaikt-name,tls-firstname,17280,whv-city,1966/09/19,wfzd-street,70,ME,MAINE +977,yshns-name,zhh-firstname,13200,dty-city,1978/12/13,hiht-street,158,KS,KANSAS +978,selvf-name,zcr-firstname,12900,phm-city,1958/12/28,pont-street,149,GU,GUAM +979,qoxyo-name,oth-firstname,10280,uic-city,1986/03/16,havi-street,153,ID,IDAHO +980,uabxl-name,cid-firstname,18000,cea-city,1982/02/18,jykd-street,127,WV,WEST VIRGINIA +981,hjnpb-name,wwh-firstname,12400,fwd-city,1978/10/01,ryqy-street,101,OH,OHIO +982,zcqum-name,ecp-firstname,15820,rpu-city,1958/11/11,fpoi-street,118,undefined,undefined +983,cetkk-name,btl-firstname,18660,xrq-city,1970/04/26,mkuo-street,167,TX,TEXAS +984,laofd-name,fqf-firstname,18180,ddy-city,1986/04/01,qjar-street,14,OR,OREGON +985,xjtie-name,tpq-firstname,16800,fgb-city,1970/03/09,nqpk-street,108,MA,MASSACHUSETTS +986,txrwc-name,ygc-firstname,17880,ipb-city,1962/04/26,zomj-street,189,DC,DISTRICT OF COLUMBIA +987,csicd-name,yzm-firstname,18240,nwu-city,1978/05/03,ishz-street,24,LA,LOUISIANA +988,njtnl-name,nxa-firstname,10360,ols-city,1990/12/01,joem-street,101,DE,DELAWARE +989,dclnv-name,hve-firstname,17080,amp-city,1978/05/22,jfuv-street,17,WI,WISCONSIN +990,jrazh-name,yec-firstname,14000,tsi-city,1986/09/02,guzz-street,119,KS,KANSAS +991,hpxko-name,psn-firstname,11900,ocf-city,1970/04/21,gvbf-street,12,OK,OKLAHOMA +992,lggcw-name,sey-firstname,17260,ike-city,1958/09/19,yrnf-street,34,NE,NEBRASKA +993,cxifr-name,agu-firstname,14360,ujj-city,1954/09/28,widd-street,127,undefined,undefined +994,kfwgh-name,qaa-firstname,17900,kis-city,1970/10/02,wwgu-street,134,MD,MARYLAND +995,pcizt-name,fdt-firstname,15320,tzn-city,1982/09/08,nqii-street,163,TN,TENNESSEE +996,hyskd-name,ott-firstname,19800,zja-city,1962/03/05,tjpp-street,108,OH,OHIO +997,nfdtl-name,jxu-firstname,12980,isx-city,1982/07/05,dldc-street,166,CA,CALIFORNIA +998,xtukc-name,aih-firstname,15360,ctn-city,1974/11/10,qhwy-street,102,MI,MICHIGAN +999,qfwcj-name,fdu-firstname,10100,enp-city,1950/01/11,qqga-street,19,TX,TEXAS +1000,uvhph-name,vri-firstname,11920,tjs-city,1974/04/28,czih-street,151,NE,NEBRASKA diff --git a/integration-tests/spark-native/datasets/0002-group-by-golden.csv b/integration-tests/spark-native/datasets/0002-group-by-golden.csv new file mode 100644 index 00000000000..0a3d842d67c --- /dev/null +++ b/integration-tests/spark-native/datasets/0002-group-by-golden.csv @@ -0,0 +1,61 @@ +state,count_all,count_distinct_city,min_id,max_id,sum_id +ALABAMA,14,14,112,872,7303 +ALASKA,19,19,1,915,9741 +AMERICAN SAMOA,15,15,15,968,7163 +ARIZONA,15,15,107,967,8194 +ARKANSAS,20,20,10,824,9160 +CALIFORNIA,19,19,12,997,8189 +COLORADO,18,18,41,804,7096 +CONNECTICUT,14,14,86,926,7561 +DELAWARE,18,18,146,988,10869 +DISTRICT OF COLUMBIA,16,16,40,986,6571 +FEDERATED STATES OF MICRONESIA,12,12,65,939,6798 +FLORIDA,20,20,59,974,8398 +GEORGIA,14,14,2,960,6183 +GUAM,19,19,57,978,11178 +HAWAII,22,22,56,895,10974 +IDAHO,14,14,71,979,8842 +ILLINOIS,13,13,250,952,8218 +INDIANA,20,20,5,759,8532 +IOWA,14,14,342,955,9589 +KANSAS,18,18,55,990,8184 +KENTUCKY,18,18,17,869,8706 +LOUISIANA,21,21,50,987,9863 +MAINE,19,19,19,976,10207 +MARSHALL ISLANDS,14,14,108,962,7536 +MARYLAND,18,18,6,994,9330 +MASSACHUSETTS,18,18,16,985,9569 +MICHIGAN,11,11,103,998,6425 +MINNESOTA,23,23,72,950,10893 +MISSISSIPPI,11,11,29,806,3870 +MISSOURI,19,19,27,954,9353 +MONTANA,19,19,14,927,7693 +NEBRASKA,20,20,132,1000,10033 +NEVADA,24,24,28,971,11354 +NEW HAMPSHIRE,14,14,11,969,6125 +NEW JERSEY,17,17,3,957,8315 +NEW MEXICO,10,10,79,830,3511 +NEW YORK,21,21,4,940,10452 +NORTH CAROLINA,16,16,9,826,5205 +NORTH DAKOTA,14,14,73,813,5510 +NORTHERN MARIANA ISLANDS,17,17,49,943,7677 +OHIO,12,12,495,996,8861 +OKLAHOMA,26,26,13,991,12922 +OREGON,12,12,131,984,7451 +PALAU,16,16,8,908,8724 +PENNSYLVANIA,15,15,7,959,7778 +PUERTO RICO,12,12,179,805,5730 +RHODE ISLAND,20,20,60,947,11856 +SOUTH CAROLINA,8,8,176,901,4093 +SOUTH DAKOTA,19,19,32,973,8887 +TENNESSEE,11,11,21,995,8301 +TEXAS,16,16,45,999,8215 +UTAH,19,19,47,975,9708 +VERMONT,16,16,77,964,7575 +VIRGIN ISLANDS,14,14,22,921,6275 +VIRGINIA,20,20,67,963,9741 +WASHINGTON,18,18,24,956,9328 +WEST VIRGINIA,22,22,48,980,10315 +WISCONSIN,17,17,18,989,10468 +WYOMING,16,16,138,844,6990 +undefined,13,13,38,993,6912 diff --git a/integration-tests/spark-native/datasets/0003-filter-rows-golden.csv b/integration-tests/spark-native/datasets/0003-filter-rows-golden.csv new file mode 100644 index 00000000000..2e331b6e570 --- /dev/null +++ b/integration-tests/spark-native/datasets/0003-filter-rows-golden.csv @@ -0,0 +1,48 @@ +id,name,firstname,zip,city,birthdate,street,housenr,stateCode,state +9,pgunz-name,hcm-firstname,16680,gxh-city,1970/11/08,shbe-street,184,NC,NORTH CAROLINA +23,eyeti-name,gnw-firstname,17420,eko-city,1962/10/26,ylph-street,61,NC,NORTH CAROLINA +49,akdmy-name,ybz-firstname,14560,wtx-city,1962/11/08,nwba-street,123,MP,NORTHERN MARIANA ISLANDS +52,snwnj-name,jyy-firstname,16400,hsz-city,1966/02/15,imhl-street,42,NC,NORTH CAROLINA +53,fuyla-name,mmp-firstname,11840,hgu-city,1986/08/16,ixiz-street,145,NC,NORTH CAROLINA +73,vkojz-name,ryo-firstname,14300,bmz-city,1954/09/11,gcpj-street,71,ND,NORTH DAKOTA +74,pqzfw-name,kld-firstname,16400,qvq-city,1962/09/09,dhbv-street,92,ND,NORTH DAKOTA +102,qfawh-name,wlx-firstname,11280,fad-city,1962/04/28,hibx-street,188,MP,NORTHERN MARIANA ISLANDS +105,oyher-name,jws-firstname,17900,bai-city,1978/12/30,tyil-street,178,NC,NORTH CAROLINA +119,qqhon-name,tlb-firstname,19880,iif-city,1982/02/05,vsbj-street,191,MP,NORTHERN MARIANA ISLANDS +136,dbvkz-name,efr-firstname,13700,ujo-city,1958/05/14,louh-street,22,MP,NORTHERN MARIANA ISLANDS +149,tdmol-name,hkb-firstname,11960,wbi-city,1970/06/03,wboh-street,59,ND,NORTH DAKOTA +159,spfxh-name,oqv-firstname,14740,gyh-city,1970/07/08,oiin-street,59,NC,NORTH CAROLINA +184,lvbqi-name,juh-firstname,12780,rst-city,1958/12/18,wwko-street,22,MP,NORTHERN MARIANA ISLANDS +195,bueoc-name,sfx-firstname,10560,xhn-city,1970/08/29,zfin-street,48,NC,NORTH CAROLINA +200,zbhue-name,ces-firstname,19840,ybc-city,1974/07/17,ktsw-street,94,ND,NORTH DAKOTA +229,airyv-name,oue-firstname,16900,gbm-city,1986/07/14,xfte-street,45,MP,NORTHERN MARIANA ISLANDS +241,opbzn-name,bxz-firstname,12860,jnq-city,1966/05/08,nkuq-street,35,MP,NORTHERN MARIANA ISLANDS +264,qchcm-name,fbc-firstname,14480,ymf-city,1978/03/21,cfqc-street,63,ND,NORTH DAKOTA +291,tndrh-name,ael-firstname,16360,cyb-city,1958/04/10,cmlg-street,78,ND,NORTH DAKOTA +299,qrlwz-name,hvk-firstname,14640,qyl-city,1954/03/07,gvsc-street,32,NC,NORTH CAROLINA +319,hqhqe-name,nxd-firstname,14860,yhi-city,1954/04/23,wkpi-street,23,MP,NORTHERN MARIANA ISLANDS +324,tfpxy-name,yzh-firstname,14080,wbc-city,1970/02/12,ojfc-street,150,MP,NORTHERN MARIANA ISLANDS +334,pxmhq-name,tyd-firstname,11200,okj-city,1978/12/30,tauk-street,94,ND,NORTH DAKOTA +381,xmnsm-name,ckj-firstname,10400,fnr-city,1950/10/21,uhme-street,154,NC,NORTH CAROLINA +393,grzby-name,oyz-firstname,15140,bmz-city,1974/07/10,rrui-street,60,NC,NORTH CAROLINA +444,zgwwi-name,umb-firstname,11840,llj-city,1958/07/06,uiww-street,115,MP,NORTHERN MARIANA ISLANDS +451,jemtu-name,gnk-firstname,11180,gbb-city,1954/03/01,pmbh-street,87,NC,NORTH CAROLINA +466,vmdwq-name,vjm-firstname,19980,rmz-city,1986/09/09,ifxg-street,139,NC,NORTH CAROLINA +472,kczhq-name,hde-firstname,16380,siz-city,1986/12/15,rawc-street,127,ND,NORTH DAKOTA +525,gabiw-name,gtj-firstname,17860,ezv-city,1978/01/18,jsyv-street,143,ND,NORTH DAKOTA +529,ipbyo-name,lcr-firstname,12040,ygz-city,1978/03/01,qbqk-street,117,ND,NORTH DAKOTA +537,nkyot-name,zep-firstname,17720,yim-city,1954/09/30,wwdl-street,198,ND,NORTH DAKOTA +575,wzura-name,fzo-firstname,12500,vgt-city,1974/06/01,tevl-street,50,NC,NORTH CAROLINA +580,uyoei-name,wiz-firstname,19000,lqo-city,1974/06/18,rvat-street,197,NC,NORTH CAROLINA +587,nqbqs-name,ndv-firstname,17500,sxr-city,1963/01/05,zclw-street,77,MP,NORTHERN MARIANA ISLANDS +604,uvovz-name,nsd-firstname,13600,tlc-city,1950/08/13,xbsm-street,110,MP,NORTHERN MARIANA ISLANDS +609,wrzts-name,ygm-firstname,15520,ybe-city,1974/12/03,uxct-street,200,ND,NORTH DAKOTA +638,crjjf-name,yfe-firstname,19880,oof-city,1970/02/07,akkj-street,98,NC,NORTH CAROLINA +640,xxugu-name,yqf-firstname,10380,ocx-city,1954/11/18,mbps-street,20,ND,NORTH DAKOTA +740,ozxng-name,sje-firstname,15180,nqf-city,1986/11/19,uivr-street,117,MP,NORTHERN MARIANA ISLANDS +813,quyjf-name,vta-firstname,14440,yiw-city,1978/01/29,nsss-street,83,ND,NORTH DAKOTA +826,bfplt-name,ard-firstname,10360,sou-city,1990/02/13,jniu-street,124,NC,NORTH CAROLINA +853,tdndt-name,qya-firstname,16320,log-city,1962/05/08,pvtk-street,151,MP,NORTHERN MARIANA ISLANDS +867,riwqf-name,qil-firstname,18480,mce-city,1990/09/07,qxyc-street,69,MP,NORTHERN MARIANA ISLANDS +936,npomk-name,upa-firstname,19060,oad-city,1986/06/27,qkua-street,200,MP,NORTHERN MARIANA ISLANDS +943,cxjtz-name,zkx-firstname,18500,mrg-city,1974/09/04,seup-street,139,MP,NORTHERN MARIANA ISLANDS diff --git a/integration-tests/spark-native/datasets/0004-stream-lookup-golden.csv b/integration-tests/spark-native/datasets/0004-stream-lookup-golden.csv new file mode 100644 index 00000000000..0fedb0c8b68 --- /dev/null +++ b/integration-tests/spark-native/datasets/0004-stream-lookup-golden.csv @@ -0,0 +1,1001 @@ +id,name,firstname,zip,city,birthdate,street,housenr,stateCode,state,countPerState +1,jwcdf-name,fsj-firstname,13520,oem-city,1954/02/07,amrb-street,145,AK,ALASKA,19 +2,flhxu-name,tum-firstname,17520,buo-city,1966/04/24,wfyz-street,96,GA,GEORGIA,14 +3,xthfg-name,gfe-firstname,12560,vtz-city,1990/01/11,doxx-street,46,NJ,NEW JERSEY,17 +4,ulzrz-name,bnl-firstname,11620,prz-city,1966/08/02,bxqn-street,104,NY,NEW YORK,21 +5,oxhyr-name,onx-firstname,15180,bpn-city,1970/11/14,pksn-street,133,IN,INDIANA,20 +6,fiqjz-name,sce-firstname,16020,fnn-city,1954/09/24,wbhg-street,35,MD,MARYLAND,18 +7,tkiat-name,xti-firstname,12720,stt-city,1966/08/11,tvnf-street,21,PA,PENNSYLVANIA,15 +8,kljcz-name,uqd-firstname,13340,ntt-city,1987/01/15,jyje-street,10,PW,PALAU,16 +9,pgunz-name,hcm-firstname,16680,gxh-city,1970/11/08,shbe-street,184,NC,NORTH CAROLINA,16 +10,oyjha-name,uhj-firstname,18880,uyg-city,1966/04/10,bjgw-street,176,AR,ARKANSAS,20 +11,igxbd-name,uph-firstname,13480,ndh-city,1962/12/03,jdcd-street,151,NH,NEW HAMPSHIRE,14 +12,vnaov-name,wha-firstname,13120,egm-city,1954/03/28,hpep-street,20,CA,CALIFORNIA,19 +13,dauuz-name,hwg-firstname,13740,khn-city,1958/05/15,etqx-street,5,OK,OKLAHOMA,26 +14,gkuuo-name,kkb-firstname,13560,xdt-city,1962/04/07,sdoj-street,35,MT,MONTANA,19 +15,wdhze-name,jjk-firstname,16900,due-city,1970/07/17,pmmu-street,174,AS,AMERICAN SAMOA,15 +16,ncayz-name,ynb-firstname,15720,lxj-city,1974/04/27,mdtb-street,109,MA,MASSACHUSETTS,18 +17,rdjin-name,hhu-firstname,14480,lpc-city,1958/11/16,wxik-street,145,KY,KENTUCKY,18 +18,nxzij-name,bdl-firstname,10740,avx-city,1958/02/20,nybz-street,138,WI,WISCONSIN,17 +19,xgrzc-name,dxw-firstname,18900,vpq-city,1990/11/16,wzjh-street,58,ME,MAINE,19 +20,ehgrn-name,vbe-firstname,17500,cik-city,1978/05/21,ucnw-street,135,MD,MARYLAND,18 +21,gctjx-name,upx-firstname,11960,yqr-city,1958/03/03,rlko-street,141,TN,TENNESSEE,11 +22,ptzmg-name,hva-firstname,15740,gux-city,1978/05/04,pugy-street,122,VI,VIRGIN ISLANDS,14 +23,eyeti-name,gnw-firstname,17420,eko-city,1962/10/26,ylph-street,61,NC,NORTH CAROLINA,16 +24,wccwo-name,zpj-firstname,16600,uim-city,1962/09/29,ygih-street,26,WA,WASHINGTON,18 +25,bwkoe-name,ayl-firstname,18660,rtw-city,1978/07/16,mzww-street,179,CA,CALIFORNIA,19 +26,rezku-name,zio-firstname,19080,nvt-city,1982/07/14,wwkd-street,91,CA,CALIFORNIA,19 +27,mjlsk-name,ecx-firstname,10800,yxu-city,1950/12/11,vttb-street,195,MO,MISSOURI,19 +28,wdjsi-name,aoq-firstname,13660,smo-city,1954/02/01,kako-street,7,NV,NEVADA,24 +29,mwfnd-name,nyb-firstname,19760,bbu-city,1986/09/23,apdi-street,91,MS,MISSISSIPPI,11 +30,vtuoz-name,jhh-firstname,17620,vad-city,1982/05/05,kzup-street,79,GA,GEORGIA,14 +31,rhhxk-name,ndr-firstname,16760,fub-city,1978/11/12,regd-street,55,OK,OKLAHOMA,26 +32,lpstk-name,mqz-firstname,18940,tnr-city,1982/09/16,cdhf-street,4,SD,SOUTH DAKOTA,19 +33,ldhyr-name,yts-firstname,12000,auk-city,1986/11/14,abph-street,147,IN,INDIANA,20 +34,cjdml-name,iti-firstname,16900,wkq-city,1970/06/05,npow-street,96,NH,NEW HAMPSHIRE,14 +35,cpenz-name,sbi-firstname,16380,ssl-city,1962/08/19,kilz-street,44,MS,MISSISSIPPI,11 +36,rxtbg-name,anr-firstname,14720,bqc-city,1958/08/10,pudg-street,140,NV,NEVADA,24 +37,udblf-name,raa-firstname,11500,wli-city,1978/12/13,xomd-street,41,PW,PALAU,16 +38,vvyce-name,gep-firstname,13740,gtd-city,1982/05/23,kwbv-street,123,undefined,undefined,13 +39,kwfnz-name,ucu-firstname,10580,sns-city,1978/08/18,nnun-street,20,OK,OKLAHOMA,26 +40,zxydx-name,tml-firstname,14680,jda-city,1974/05/29,wfjn-street,157,DC,DISTRICT OF COLUMBIA,16 +41,bfscx-name,jnl-firstname,16920,yyg-city,1970/11/30,cgfh-street,178,CO,COLORADO,18 +42,qitur-name,yra-firstname,15560,ijp-city,1978/01/30,fonc-street,155,AK,ALASKA,19 +43,msixi-name,ynb-firstname,12720,ksl-city,1958/07/17,zpjw-street,46,VI,VIRGIN ISLANDS,14 +44,wzkjq-name,rgh-firstname,19000,hkm-city,1974/08/12,yixf-street,134,CA,CALIFORNIA,19 +45,dqfmf-name,yxr-firstname,13840,vie-city,1962/10/23,stvx-street,39,TX,TEXAS,16 +46,biluz-name,uqe-firstname,17760,wkq-city,1962/07/27,embn-street,183,PW,PALAU,16 +47,wahfx-name,zwd-firstname,13240,vic-city,1974/03/27,axpw-street,131,UT,UTAH,19 +48,denwt-name,bta-firstname,17300,hhj-city,1986/12/20,orwy-street,11,WV,WEST VIRGINIA,22 +49,akdmy-name,ybz-firstname,14560,wtx-city,1962/11/08,nwba-street,123,MP,NORTHERN MARIANA ISLANDS,17 +50,hqafg-name,nht-firstname,16080,gfu-city,1951/01/12,spsq-street,45,LA,LOUISIANA,21 +51,zhmbl-name,lnw-firstname,17460,hse-city,1986/12/21,scis-street,97,GA,GEORGIA,14 +52,snwnj-name,jyy-firstname,16400,hsz-city,1966/02/15,imhl-street,42,NC,NORTH CAROLINA,16 +53,fuyla-name,mmp-firstname,11840,hgu-city,1986/08/16,ixiz-street,145,NC,NORTH CAROLINA,16 +54,yvfqz-name,prz-firstname,11260,wjl-city,1982/05/06,fbzd-street,97,MO,MISSOURI,19 +55,usbgq-name,vhd-firstname,14080,dsb-city,1958/04/01,ggoc-street,54,KS,KANSAS,18 +56,yaeni-name,zpy-firstname,19100,sen-city,1954/12/10,sbsw-street,158,HI,HAWAII,22 +57,fgxvr-name,vzi-firstname,17520,lcf-city,1958/11/01,nbdv-street,10,GU,GUAM,19 +58,tqpbq-name,rwr-firstname,19140,zpd-city,1978/08/23,npvb-street,190,DC,DISTRICT OF COLUMBIA,16 +59,ieigg-name,ayq-firstname,12960,ljc-city,1962/07/05,dnjz-street,163,FL,FLORIDA,20 +60,rfvzu-name,edm-firstname,13340,kvz-city,1954/12/08,eijd-street,4,RI,RHODE ISLAND,20 +61,pduwm-name,gqb-firstname,14240,cyr-city,1954/07/03,ndux-street,13,SD,SOUTH DAKOTA,19 +62,yyixf-name,yzt-firstname,18020,lwx-city,1974/01/29,iede-street,120,NV,NEVADA,24 +63,dkszq-name,ytd-firstname,14700,zwh-city,1979/01/11,nbjz-street,65,AS,AMERICAN SAMOA,15 +64,slkzv-name,zbg-firstname,19880,oee-city,1978/11/01,sphg-street,119,OK,OKLAHOMA,26 +65,nvxim-name,phc-firstname,19220,vgg-city,1991/01/24,juok-street,106,FM,FEDERATED STATES OF MICRONESIA,12 +66,piyfg-name,xtn-firstname,13760,nde-city,1954/07/22,vfrv-street,11,NY,NEW YORK,21 +67,jnusz-name,mjw-firstname,12640,nwb-city,1986/08/23,kcsa-street,138,VA,VIRGINIA,20 +68,jnypj-name,ioq-firstname,17000,zqy-city,1986/01/09,croe-street,119,PW,PALAU,16 +69,uohts-name,btx-firstname,13480,dal-city,1990/10/22,llyw-street,150,WA,WASHINGTON,18 +70,aavpj-name,pvw-firstname,13780,lai-city,1954/09/23,nygu-street,171,FL,FLORIDA,20 +71,nbjcj-name,rsf-firstname,12000,kjl-city,1986/06/30,ijsb-street,123,ID,IDAHO,14 +72,syjxh-name,gkq-firstname,19960,rmd-city,1978/10/26,qmyp-street,161,MN,MINNESOTA,23 +73,vkojz-name,ryo-firstname,14300,bmz-city,1954/09/11,gcpj-street,71,ND,NORTH DAKOTA,14 +74,pqzfw-name,kld-firstname,16400,qvq-city,1962/09/09,dhbv-street,92,ND,NORTH DAKOTA,14 +75,owvjk-name,fez-firstname,19740,ldb-city,1978/06/14,kabf-street,87,VA,VIRGINIA,20 +76,qsfih-name,ixe-firstname,16860,qvr-city,1987/01/07,qean-street,159,CO,COLORADO,18 +77,slixq-name,gmb-firstname,19980,ftt-city,1982/06/22,xinx-street,111,VT,VERMONT,16 +78,eegsa-name,xlc-firstname,12680,byk-city,1954/04/23,beul-street,56,MD,MARYLAND,18 +79,phevp-name,ihs-firstname,16120,adc-city,1978/04/25,voig-street,98,NM,NEW MEXICO,10 +80,njfoe-name,tag-firstname,16580,tnr-city,1966/12/04,dhky-street,108,LA,LOUISIANA,21 +81,bdncx-name,hcd-firstname,11260,xcl-city,1970/07/02,jvlp-street,49,GA,GEORGIA,14 +82,ikedo-name,tks-firstname,17460,odl-city,1958/08/25,iaaq-street,8,GU,GUAM,19 +83,iafxy-name,vur-firstname,11480,hgt-city,1962/08/03,hmec-street,164,TX,TEXAS,16 +84,lafhf-name,ssz-firstname,19560,wwp-city,1951/01/25,mxmq-street,96,IN,INDIANA,20 +85,okyny-name,hbu-firstname,16800,yok-city,1978/03/28,ipjz-street,135,NV,NEVADA,24 +86,hznby-name,fwy-firstname,13680,wbi-city,1970/07/25,mxui-street,170,CT,CONNECTICUT,14 +87,ztpoa-name,rzk-firstname,18500,qum-city,1970/07/26,blqr-street,152,ME,MAINE,19 +88,gitxz-name,axt-firstname,11800,fck-city,1974/01/12,tmjw-street,189,SD,SOUTH DAKOTA,19 +89,ziomm-name,mcv-firstname,12940,iwq-city,1950/10/22,hqgj-street,140,DC,DISTRICT OF COLUMBIA,16 +90,otncg-name,tuy-firstname,16540,ulk-city,1971/01/24,yuia-street,166,TX,TEXAS,16 +91,cnabb-name,hoq-firstname,16300,tuw-city,1962/06/17,ujvv-street,61,ME,MAINE,19 +92,ucogf-name,ggc-firstname,14500,fsj-city,1978/02/08,asfi-street,53,WV,WEST VIRGINIA,22 +93,lbpmf-name,sdt-firstname,10780,ewj-city,1978/03/08,hxsp-street,102,NV,NEVADA,24 +94,tieqq-name,uyu-firstname,17740,wea-city,1966/10/31,abpl-street,187,MO,MISSOURI,19 +95,fsgwf-name,vjd-firstname,12460,ads-city,1970/11/29,yeou-street,10,MA,MASSACHUSETTS,18 +96,reeba-name,kzs-firstname,13100,zhc-city,1966/07/08,abmv-street,88,FL,FLORIDA,20 +97,shybc-name,gcp-firstname,10660,ahg-city,1950/12/15,hrqy-street,174,KS,KANSAS,18 +98,phszr-name,sst-firstname,13080,ydd-city,1954/09/23,quqn-street,2,RI,RHODE ISLAND,20 +99,jteco-name,fxc-firstname,19760,agr-city,1986/05/06,dzxc-street,108,MD,MARYLAND,18 +100,qvaar-name,icx-firstname,16120,boc-city,1978/08/04,bfzf-street,12,NM,NEW MEXICO,10 +101,iiapu-name,veo-firstname,10180,wdv-city,1954/05/29,ovyu-street,55,WI,WISCONSIN,17 +102,qfawh-name,wlx-firstname,11280,fad-city,1962/04/28,hibx-street,188,MP,NORTHERN MARIANA ISLANDS,17 +103,nfurq-name,rib-firstname,17080,xcp-city,1962/11/10,rqui-street,164,MI,MICHIGAN,11 +104,hdbiw-name,wxm-firstname,12600,txy-city,1978/11/23,yfcx-street,112,OK,OKLAHOMA,26 +105,oyher-name,jws-firstname,17900,bai-city,1978/12/30,tyil-street,178,NC,NORTH CAROLINA,16 +106,fzjnb-name,wxk-firstname,12300,fda-city,1974/03/26,aweg-street,7,MO,MISSOURI,19 +107,ycpcp-name,xfq-firstname,12660,mna-city,1986/02/14,dcki-street,5,AZ,ARIZONA,15 +108,rxxeb-name,qdw-firstname,17600,yks-city,1970/11/15,zvsf-street,74,MH,MARSHALL ISLANDS,14 +109,ffbgl-name,fqf-firstname,11680,npo-city,1974/12/07,vvan-street,175,GA,GEORGIA,14 +110,foygy-name,vog-firstname,16920,mun-city,1970/07/03,urct-street,153,HI,HAWAII,22 +111,imoqe-name,xfe-firstname,14620,gfv-city,1986/02/20,nlak-street,181,AK,ALASKA,19 +112,mnhwk-name,bbt-firstname,16180,bnf-city,1986/10/10,yybd-street,144,AL,ALABAMA,14 +113,lkfyf-name,xhg-firstname,13260,myb-city,1958/10/27,jjcn-street,128,GA,GEORGIA,14 +114,bkhya-name,hrh-firstname,11500,byw-city,1990/03/03,oubr-street,41,MH,MARSHALL ISLANDS,14 +115,avceb-name,ztu-firstname,10020,ogo-city,1974/05/26,iuob-street,31,KS,KANSAS,18 +116,pnacg-name,iws-firstname,11640,dtx-city,1966/06/01,kwwj-street,177,WV,WEST VIRGINIA,22 +117,ypnsc-name,tyv-firstname,16820,glg-city,1962/12/19,nysz-street,13,OK,OKLAHOMA,26 +118,agndn-name,qae-firstname,15540,tpg-city,1990/05/15,ygzx-street,166,NV,NEVADA,24 +119,qqhon-name,tlb-firstname,19880,iif-city,1982/02/05,vsbj-street,191,MP,NORTHERN MARIANA ISLANDS,17 +120,qzxic-name,mot-firstname,14840,qvf-city,1970/03/19,gelo-street,149,WA,WASHINGTON,18 +121,lunca-name,ced-firstname,13700,wjp-city,1979/01/06,racn-street,54,ID,IDAHO,14 +122,jsxhg-name,yoo-firstname,13440,ulf-city,1982/06/28,krry-street,55,NY,NEW YORK,21 +123,ugbci-name,vht-firstname,17100,ffc-city,1962/10/16,ixha-street,99,VT,VERMONT,16 +124,hgtkz-name,xgg-firstname,17500,qck-city,1978/07/17,xkez-street,60,AK,ALASKA,19 +125,ejdoc-name,ovv-firstname,14920,ocs-city,1954/11/15,bwhd-street,169,FL,FLORIDA,20 +126,zoucr-name,ivo-firstname,14040,grt-city,1990/04/29,qhox-street,90,WI,WISCONSIN,17 +127,asofx-name,yzv-firstname,19600,ixo-city,1982/07/09,slmy-street,198,MO,MISSOURI,19 +128,tgowi-name,zvm-firstname,17560,avv-city,1986/11/04,qnyp-street,83,AS,AMERICAN SAMOA,15 +129,toeur-name,ydp-firstname,17260,dpz-city,1962/11/02,atag-street,60,IN,INDIANA,20 +130,eohex-name,vfp-firstname,17000,gzc-city,1970/01/05,cvlk-street,188,UT,UTAH,19 +131,mahci-name,cwt-firstname,18240,wut-city,1982/08/02,zyse-street,147,OR,OREGON,12 +132,hzetq-name,kif-firstname,14280,aep-city,1990/11/26,rrsr-street,118,NE,NEBRASKA,20 +133,tjrgp-name,vle-firstname,15620,sdv-city,1962/12/08,zdyx-street,88,WV,WEST VIRGINIA,22 +134,japyi-name,jkm-firstname,14960,jeo-city,1958/03/01,bsxv-street,86,TX,TEXAS,16 +135,eqley-name,ttv-firstname,12740,trs-city,1974/09/16,ibff-street,187,CA,CALIFORNIA,19 +136,dbvkz-name,efr-firstname,13700,ujo-city,1958/05/14,louh-street,22,MP,NORTHERN MARIANA ISLANDS,17 +137,mpgcz-name,ysk-firstname,11860,eva-city,1978/08/23,nedx-street,101,MT,MONTANA,19 +138,bntsw-name,osn-firstname,15700,mmv-city,1966/07/28,loqs-street,34,WY,WYOMING,16 +139,yfcbv-name,ing-firstname,10300,ddb-city,1966/04/12,amuj-street,32,SD,SOUTH DAKOTA,19 +140,ddwqy-name,rxo-firstname,18720,nsh-city,1974/06/22,rugn-street,105,LA,LOUISIANA,21 +141,zuxwq-name,xha-firstname,12240,jii-city,1974/04/22,kawh-street,97,NJ,NEW JERSEY,17 +142,aarej-name,dfg-firstname,14680,exu-city,1958/04/17,adua-street,11,NE,NEBRASKA,20 +143,iezfw-name,ufb-firstname,18800,fyv-city,1970/06/05,yvao-street,53,HI,HAWAII,22 +144,vvmzr-name,bud-firstname,15120,ggo-city,1966/07/24,ozcj-street,127,MT,MONTANA,19 +145,bknbv-name,qrd-firstname,11500,mth-city,1970/04/16,ijle-street,143,NH,NEW HAMPSHIRE,14 +146,bwyxl-name,fdq-firstname,13160,ngn-city,1954/07/05,nkco-street,120,DE,DELAWARE,18 +147,lkkwb-name,yqh-firstname,19580,pwn-city,1954/10/16,rgdl-street,185,MN,MINNESOTA,23 +148,uokzd-name,aco-firstname,13940,wyf-city,1966/02/07,lbhd-street,23,NH,NEW HAMPSHIRE,14 +149,tdmol-name,hkb-firstname,11960,wbi-city,1970/06/03,wboh-street,59,ND,NORTH DAKOTA,14 +150,erulk-name,xcd-firstname,11420,kzt-city,1990/02/07,bmcb-street,160,DC,DISTRICT OF COLUMBIA,16 +151,atrip-name,mlq-firstname,14440,agk-city,1986/11/08,qhdv-street,29,IN,INDIANA,20 +152,atiir-name,brc-firstname,11380,sas-city,1958/10/20,dwyv-street,100,AR,ARKANSAS,20 +153,cvygb-name,kdu-firstname,17300,etl-city,1954/11/13,bdxo-street,43,MA,MASSACHUSETTS,18 +154,jeyeq-name,yjl-firstname,12740,jgr-city,1978/06/21,aavd-street,61,NY,NEW YORK,21 +155,wojgm-name,xdk-firstname,13340,meq-city,1982/10/20,ttix-street,61,MT,MONTANA,19 +156,oktge-name,taf-firstname,11200,ibx-city,1990/09/05,clbk-street,70,UT,UTAH,19 +157,cdrrm-name,dmu-firstname,11980,bqa-city,1962/06/18,owlk-street,26,NY,NEW YORK,21 +158,jyqvl-name,rht-firstname,11120,qrk-city,1982/04/20,qbrn-street,55,WY,WYOMING,16 +159,spfxh-name,oqv-firstname,14740,gyh-city,1970/07/08,oiin-street,59,NC,NORTH CAROLINA,16 +160,sczwy-name,mhg-firstname,17860,izz-city,1970/08/25,xehg-street,2,NJ,NEW JERSEY,17 +161,lklcm-name,rcy-firstname,11960,ycf-city,1982/07/04,path-street,179,NJ,NEW JERSEY,17 +162,jcluy-name,tlk-firstname,10380,lsi-city,1970/03/17,ugqr-street,138,NJ,NEW JERSEY,17 +163,qqdvp-name,hsh-firstname,18240,bqf-city,1982/01/01,nupe-street,153,LA,LOUISIANA,21 +164,rxlox-name,uoi-firstname,15600,uvd-city,1954/04/03,gjmv-street,197,MA,MASSACHUSETTS,18 +165,kjypq-name,wgt-firstname,14060,yrs-city,1978/06/09,ijks-street,144,CO,COLORADO,18 +166,zegdj-name,fpi-firstname,13380,znp-city,1978/04/26,pdlh-street,187,KY,KENTUCKY,18 +167,ihkkq-name,gtk-firstname,15740,qbg-city,1970/06/18,odsg-street,95,CO,COLORADO,18 +168,yhnuk-name,uhh-firstname,16720,hoo-city,1978/06/20,vrcy-street,186,KS,KANSAS,18 +169,ftpvt-name,ufk-firstname,13600,wat-city,1954/10/16,nxax-street,112,OR,OREGON,12 +170,xoiyz-name,xqq-firstname,14560,kea-city,1986/08/10,bivl-street,177,MN,MINNESOTA,23 +171,wfeuq-name,qec-firstname,16540,obq-city,1950/11/17,keyf-street,108,UT,UTAH,19 +172,pfrmg-name,tyi-firstname,15360,tjx-city,1979/01/30,lyhr-street,78,NE,NEBRASKA,20 +173,najqw-name,ldk-firstname,10220,bci-city,1982/04/01,qxuf-street,84,MS,MISSISSIPPI,11 +174,qbqrg-name,zyo-firstname,13420,cdh-city,1958/06/13,gqst-street,167,WY,WYOMING,16 +175,lnaxv-name,zwt-firstname,14740,lok-city,1962/10/06,mmdu-street,149,KY,KENTUCKY,18 +176,tpgpm-name,qie-firstname,14960,opy-city,1958/07/14,uxfv-street,158,SC,SOUTH CAROLINA,8 +177,nltvw-name,ahc-firstname,19520,uxf-city,1958/03/16,fwsy-street,131,CA,CALIFORNIA,19 +178,ujfpc-name,cwd-firstname,13800,gki-city,1974/10/10,sgiq-street,12,FL,FLORIDA,20 +179,pehcm-name,mah-firstname,15940,azs-city,1970/05/07,hvvk-street,9,PR,PUERTO RICO,12 +180,phlqr-name,qog-firstname,12160,qvt-city,1966/09/11,isol-street,155,AZ,ARIZONA,15 +181,bxerk-name,kxv-firstname,14180,sek-city,1982/02/18,ctwu-street,84,CA,CALIFORNIA,19 +182,nmlqw-name,oyf-firstname,12640,tmv-city,1962/02/26,eqss-street,141,NE,NEBRASKA,20 +183,kobcl-name,pht-firstname,15820,nky-city,1978/05/14,vfnd-street,176,PR,PUERTO RICO,12 +184,lvbqi-name,juh-firstname,12780,rst-city,1958/12/18,wwko-street,22,MP,NORTHERN MARIANA ISLANDS,17 +185,yqqmt-name,zrg-firstname,12780,hxs-city,1954/08/12,mdxh-street,190,MS,MISSISSIPPI,11 +186,osbyt-name,qtk-firstname,14900,ltd-city,1990/06/21,quqn-street,59,MO,MISSOURI,19 +187,vibab-name,vgy-firstname,19600,jxa-city,1958/09/20,czps-street,137,AR,ARKANSAS,20 +188,vrnml-name,qmd-firstname,15860,mxe-city,1966/07/23,ybfc-street,148,DE,DELAWARE,18 +189,thimt-name,ige-firstname,12900,dqn-city,1966/05/07,bccw-street,187,OK,OKLAHOMA,26 +190,jdzou-name,qnd-firstname,17600,fzi-city,1958/06/12,ewtx-street,174,IN,INDIANA,20 +191,bsvvw-name,hfa-firstname,14180,kmn-city,1974/09/19,zvdw-street,13,UT,UTAH,19 +192,iwbao-name,qur-firstname,19500,jlk-city,1982/08/08,kllj-street,113,WA,WASHINGTON,18 +193,xpxla-name,yzv-firstname,19020,eze-city,1954/04/22,taku-street,105,AS,AMERICAN SAMOA,15 +194,gqugh-name,sdy-firstname,14360,pwi-city,1974/03/11,qybh-street,95,KY,KENTUCKY,18 +195,bueoc-name,sfx-firstname,10560,xhn-city,1970/08/29,zfin-street,48,NC,NORTH CAROLINA,16 +196,fyrln-name,fay-firstname,10820,qtd-city,1974/11/04,yrtc-street,120,MH,MARSHALL ISLANDS,14 +197,zuhli-name,qwr-firstname,19800,nqp-city,1970/01/21,mdew-street,8,VA,VIRGINIA,20 +198,rwplk-name,jkr-firstname,18080,khf-city,1978/02/28,ihkv-street,134,MT,MONTANA,19 +199,ssbzy-name,azn-firstname,11440,ire-city,1954/11/16,sjou-street,55,CO,COLORADO,18 +200,zbhue-name,ces-firstname,19840,ybc-city,1974/07/17,ktsw-street,94,ND,NORTH DAKOTA,14 +201,tcpnt-name,tgk-firstname,19580,nox-city,1990/02/24,rmst-street,59,MS,MISSISSIPPI,11 +202,avrpe-name,aaz-firstname,14000,anm-city,1950/09/02,ddjz-street,197,FL,FLORIDA,20 +203,qemau-name,lbl-firstname,15620,jkx-city,1962/07/23,kxdn-street,38,PA,PENNSYLVANIA,15 +204,xzeqe-name,bjx-firstname,12960,qiv-city,1958/09/07,yohx-street,22,VA,VIRGINIA,20 +205,ufbqh-name,dcm-firstname,17720,tch-city,1978/10/16,sqis-street,119,NM,NEW MEXICO,10 +206,cfwje-name,kng-firstname,15980,hmf-city,1974/09/23,timl-street,105,NV,NEVADA,24 +207,dpswi-name,lzu-firstname,11020,mby-city,1962/10/14,stnj-street,143,UT,UTAH,19 +208,padrh-name,yvj-firstname,17680,pqc-city,1986/11/28,xmxq-street,81,GU,GUAM,19 +209,blcun-name,erh-firstname,16200,sgc-city,1950/10/10,sqkp-street,29,PA,PENNSYLVANIA,15 +210,jxuox-name,ztl-firstname,13140,hox-city,1962/08/12,vxgj-street,83,KS,KANSAS,18 +211,bcfua-name,urk-firstname,11540,mhn-city,1982/10/09,poor-street,21,VI,VIRGIN ISLANDS,14 +212,nmccn-name,nlv-firstname,11780,dec-city,1974/07/05,txyt-street,125,FL,FLORIDA,20 +213,cwcfl-name,nye-firstname,10620,ciu-city,1974/06/14,dumh-street,124,TX,TEXAS,16 +214,bewhj-name,mcq-firstname,16040,vir-city,1951/01/31,uhse-street,78,MA,MASSACHUSETTS,18 +215,owbls-name,mcq-firstname,12940,zjk-city,1962/08/21,plgy-street,185,NJ,NEW JERSEY,17 +216,tjwtx-name,uur-firstname,13400,jqa-city,1962/05/15,hhzi-street,134,KS,KANSAS,18 +217,aynmw-name,obp-firstname,13820,hbu-city,1974/12/09,lfeb-street,28,NV,NEVADA,24 +218,aivbc-name,fkc-firstname,14980,mew-city,1958/05/19,rxqg-street,41,AK,ALASKA,19 +219,nkeha-name,ddi-firstname,17680,wzu-city,1986/03/04,xtik-street,11,RI,RHODE ISLAND,20 +220,hyyqm-name,vac-firstname,17600,gph-city,1954/04/28,rjxi-street,22,NY,NEW YORK,21 +221,sifar-name,yth-firstname,12840,kbe-city,1982/11/18,mnje-street,5,NY,NEW YORK,21 +222,bxlyk-name,pla-firstname,15740,zpb-city,1990/12/07,viys-street,171,HI,HAWAII,22 +223,ifcwk-name,yfu-firstname,10000,itv-city,1982/06/16,iuya-street,77,SD,SOUTH DAKOTA,19 +224,fherw-name,acw-firstname,13000,dxg-city,1970/09/09,zscv-street,45,VT,VERMONT,16 +225,cvbfh-name,tbs-firstname,13160,znt-city,1958/07/20,exfl-street,171,LA,LOUISIANA,21 +226,sxpuq-name,qdu-firstname,13000,lhm-city,1971/01/08,yooq-street,80,VT,VERMONT,16 +227,xawor-name,glz-firstname,18160,dxx-city,1954/12/08,fjnf-street,130,FM,FEDERATED STATES OF MICRONESIA,12 +228,dwpda-name,dtg-firstname,15380,zyz-city,1974/04/21,gozg-street,96,MT,MONTANA,19 +229,airyv-name,oue-firstname,16900,gbm-city,1986/07/14,xfte-street,45,MP,NORTHERN MARIANA ISLANDS,17 +230,omfog-name,zhv-firstname,17020,lep-city,1970/03/21,trww-street,128,LA,LOUISIANA,21 +231,ddgah-name,ost-firstname,13580,ojl-city,1958/03/07,gnln-street,155,KY,KENTUCKY,18 +232,feggq-name,cro-firstname,18780,wtj-city,1966/10/01,jesi-street,63,NM,NEW MEXICO,10 +233,ahxvq-name,nes-firstname,11660,niu-city,1950/06/06,upyk-street,185,WA,WASHINGTON,18 +234,gjlqf-name,mvv-firstname,19620,roc-city,1974/05/01,tsqu-street,19,MI,MICHIGAN,11 +235,xbcip-name,vyn-firstname,10560,nru-city,1986/12/06,qxfi-street,114,WI,WISCONSIN,17 +236,cdwuj-name,sks-firstname,12560,typ-city,1954/01/31,fkwb-street,128,DC,DISTRICT OF COLUMBIA,16 +237,bsekx-name,wbw-firstname,14280,twm-city,1962/09/11,bxui-street,174,NM,NEW MEXICO,10 +238,foppd-name,zlw-firstname,13580,hmg-city,1974/04/29,yiwk-street,68,MN,MINNESOTA,23 +239,brtej-name,cqi-firstname,11000,elz-city,1982/10/16,uauh-street,23,PA,PENNSYLVANIA,15 +240,cpklp-name,tps-firstname,11440,nsm-city,1950/10/28,cmjv-street,139,PR,PUERTO RICO,12 +241,opbzn-name,bxz-firstname,12860,jnq-city,1966/05/08,nkuq-street,35,MP,NORTHERN MARIANA ISLANDS,17 +242,kxkmx-name,ziy-firstname,17460,wqq-city,1974/11/05,gnha-street,192,WV,WEST VIRGINIA,22 +243,lxpbu-name,jph-firstname,19500,fpa-city,1954/10/01,gdls-street,163,MI,MICHIGAN,11 +244,erhwd-name,wvu-firstname,11880,iza-city,1962/08/03,ucsh-street,75,undefined,undefined,13 +245,tjuxy-name,jzf-firstname,10580,nyq-city,1970/04/30,gbes-street,189,AR,ARKANSAS,20 +246,piocq-name,skz-firstname,14600,xuq-city,1978/07/12,inae-street,27,ME,MAINE,19 +247,bqjty-name,ybj-firstname,13040,jqu-city,1954/11/30,ugcn-street,159,AL,ALABAMA,14 +248,xexrx-name,fpu-firstname,19720,ckc-city,1974/06/30,zpez-street,46,IN,INDIANA,20 +249,auzgu-name,dam-firstname,18460,mih-city,1962/07/28,augp-street,112,MD,MARYLAND,18 +250,xupoe-name,fdb-firstname,13440,llr-city,1986/10/01,forq-street,185,IL,ILLINOIS,13 +251,sgely-name,pzz-firstname,15920,jya-city,1950/02/02,kypg-street,147,DC,DISTRICT OF COLUMBIA,16 +252,onini-name,zts-firstname,18060,avs-city,1974/12/02,kxjn-street,85,TX,TEXAS,16 +253,tyflk-name,htl-firstname,17560,bhd-city,1962/06/06,xquf-street,126,IN,INDIANA,20 +254,chbez-name,zkj-firstname,17000,goh-city,1974/03/02,rkui-street,13,AR,ARKANSAS,20 +255,zmmhg-name,rqb-firstname,11340,egt-city,1970/02/06,pwjj-street,6,MH,MARSHALL ISLANDS,14 +256,gutfo-name,vki-firstname,18860,xdv-city,1986/07/11,iwkf-street,14,DC,DISTRICT OF COLUMBIA,16 +257,eogwr-name,hnt-firstname,19840,nht-city,1990/05/02,kljr-street,18,VT,VERMONT,16 +258,ibupi-name,ygc-firstname,18580,tvk-city,1978/08/20,xphm-street,123,CO,COLORADO,18 +259,wqpaq-name,uwc-firstname,10780,ygl-city,1958/03/20,ncta-street,87,GU,GUAM,19 +260,swcms-name,ljb-firstname,13560,pvt-city,1954/12/13,ovsf-street,176,MD,MARYLAND,18 +261,xwxmt-name,qpq-firstname,13380,fzc-city,1978/10/14,ivwb-street,17,AZ,ARIZONA,15 +262,drxej-name,msd-firstname,12720,ure-city,1986/02/15,nrqa-street,30,MT,MONTANA,19 +263,scgpq-name,wgg-firstname,18740,xtx-city,1990/05/09,vxwl-street,29,AK,ALASKA,19 +264,qchcm-name,fbc-firstname,14480,ymf-city,1978/03/21,cfqc-street,63,ND,NORTH DAKOTA,14 +265,cbpxm-name,clb-firstname,16900,etx-city,1974/03/29,uzyw-street,175,OK,OKLAHOMA,26 +266,clksy-name,mls-firstname,13560,qhs-city,1958/04/07,trwx-street,47,MO,MISSOURI,19 +267,qwagv-name,hil-firstname,18240,atx-city,1954/01/30,glaf-street,57,MT,MONTANA,19 +268,grkjm-name,qwy-firstname,15900,jvu-city,1970/02/26,rhdn-street,172,ME,MAINE,19 +269,gkiis-name,xhp-firstname,14440,bgh-city,1954/01/02,btrx-street,53,FL,FLORIDA,20 +270,nscdn-name,lfn-firstname,19900,eph-city,1958/09/29,nqao-street,171,KS,KANSAS,18 +271,ulrrc-name,ncb-firstname,10320,cao-city,1986/10/15,lkrm-street,125,GA,GEORGIA,14 +272,qxjcn-name,wpy-firstname,11260,rew-city,1954/12/19,tldv-street,115,CA,CALIFORNIA,19 +273,thyyj-name,htd-firstname,19100,tae-city,1982/03/04,wbqv-street,174,PR,PUERTO RICO,12 +274,uigiq-name,lmx-firstname,19320,bkr-city,1950/04/07,foio-street,104,OK,OKLAHOMA,26 +275,iinbr-name,cfg-firstname,12100,qwv-city,1986/01/12,ervo-street,87,MN,MINNESOTA,23 +276,kipbw-name,fda-firstname,16300,mlu-city,1970/03/26,yjpd-street,15,MN,MINNESOTA,23 +277,cfrgd-name,dui-firstname,11320,jax-city,1990/04/14,zaik-street,157,ME,MAINE,19 +278,duaza-name,xsx-firstname,11120,kkg-city,1958/02/05,bply-street,185,VI,VIRGIN ISLANDS,14 +279,zzhqx-name,vlb-firstname,18380,vyb-city,1950/12/11,iugj-street,159,HI,HAWAII,22 +280,kuryf-name,kib-firstname,13140,tum-city,1954/05/06,clkw-street,31,CO,COLORADO,18 +281,daljt-name,ycr-firstname,10840,ckw-city,1982/10/29,umof-street,52,CO,COLORADO,18 +282,xhssg-name,djc-firstname,17840,gvj-city,1954/12/12,zgmw-street,35,WV,WEST VIRGINIA,22 +283,qqscv-name,eiu-firstname,15260,daj-city,1986/01/30,qprc-street,158,MH,MARSHALL ISLANDS,14 +284,tehbb-name,czj-firstname,14180,xnh-city,1958/03/06,lfji-street,48,UT,UTAH,19 +285,acdsd-name,yiu-firstname,12220,buk-city,1958/02/15,ovcj-street,70,ME,MAINE,19 +286,derpl-name,buv-firstname,16000,fha-city,1970/09/02,vfay-street,57,WA,WASHINGTON,18 +287,lobqd-name,qxn-firstname,15060,ycp-city,1966/12/24,axxb-street,38,PA,PENNSYLVANIA,15 +288,owxja-name,okb-firstname,15640,lad-city,1982/05/24,wnka-street,101,MN,MINNESOTA,23 +289,zohkw-name,ypo-firstname,15460,wtu-city,1978/08/10,jhco-street,21,VA,VIRGINIA,20 +290,nmrar-name,aqm-firstname,18360,lcn-city,1963/01/26,zxeo-street,143,WV,WEST VIRGINIA,22 +291,tndrh-name,ael-firstname,16360,cyb-city,1958/04/10,cmlg-street,78,ND,NORTH DAKOTA,14 +292,kbpkv-name,yck-firstname,19400,oka-city,1974/11/02,kpmc-street,10,CO,COLORADO,18 +293,uabcl-name,zms-firstname,15940,tzb-city,1970/08/06,ezzs-street,11,VT,VERMONT,16 +294,lancp-name,zbk-firstname,11560,vny-city,1978/07/21,tzys-street,182,LA,LOUISIANA,21 +295,wagzv-name,hcp-firstname,13760,kik-city,1974/11/01,gpuw-street,151,NH,NEW HAMPSHIRE,14 +296,qrenr-name,egp-firstname,16920,zwq-city,1966/03/19,fbwi-street,102,FL,FLORIDA,20 +297,amjep-name,mds-firstname,16700,fvb-city,1978/05/07,peau-street,167,WV,WEST VIRGINIA,22 +298,dzppv-name,qav-firstname,16680,wbc-city,1970/04/15,dzyp-street,149,VI,VIRGIN ISLANDS,14 +299,qrlwz-name,hvk-firstname,14640,qyl-city,1954/03/07,gvsc-street,32,NC,NORTH CAROLINA,16 +300,lracx-name,dmp-firstname,13800,slo-city,1970/09/21,jspb-street,106,WA,WASHINGTON,18 +301,jhlao-name,txt-firstname,12020,jam-city,1962/06/26,bcky-street,19,WV,WEST VIRGINIA,22 +302,aocxq-name,mzv-firstname,17140,dgx-city,1954/08/24,maoi-street,1,HI,HAWAII,22 +303,dvdbu-name,fdf-firstname,10980,goa-city,1982/04/10,onik-street,109,NM,NEW MEXICO,10 +304,cqapx-name,skq-firstname,11080,akw-city,1958/09/02,xakx-street,14,CA,CALIFORNIA,19 +305,crmlf-name,djf-firstname,16100,mle-city,1974/10/10,udrl-street,96,VT,VERMONT,16 +306,iujcn-name,tat-firstname,17660,jnf-city,1966/03/31,inoh-street,104,TX,TEXAS,16 +307,aetvh-name,spn-firstname,14960,wxf-city,1954/06/30,mmxe-street,78,PA,PENNSYLVANIA,15 +308,ibfdt-name,sfu-firstname,11120,kqf-city,1954/06/22,xbwg-street,132,CA,CALIFORNIA,19 +309,ccatv-name,aeb-firstname,10940,qoi-city,1982/08/11,bsrg-street,29,UT,UTAH,19 +310,udrjw-name,agc-firstname,17100,aep-city,1974/04/23,bsju-street,59,PR,PUERTO RICO,12 +311,crjcx-name,nbb-firstname,14820,xtp-city,1958/11/08,fajh-street,95,WY,WYOMING,16 +312,qbcrw-name,mef-firstname,14220,mwa-city,1982/11/13,keyy-street,97,NE,NEBRASKA,20 +313,wxvpi-name,dym-firstname,10220,ncp-city,1954/09/28,qgrg-street,25,undefined,undefined,13 +314,bandc-name,hzf-firstname,18700,eoh-city,1990/11/05,qphi-street,177,IL,ILLINOIS,13 +315,afatf-name,eii-firstname,15480,lsm-city,1982/10/11,nzjq-street,96,VI,VIRGIN ISLANDS,14 +316,trxge-name,vhe-firstname,18260,ccm-city,1958/03/09,ducm-street,1,NH,NEW HAMPSHIRE,14 +317,kdygt-name,tgc-firstname,12500,bqo-city,1978/06/20,rqgx-street,90,DC,DISTRICT OF COLUMBIA,16 +318,angdt-name,lvt-firstname,13960,tbr-city,1974/07/14,wfqj-street,196,GU,GUAM,19 +319,hqhqe-name,nxd-firstname,14860,yhi-city,1954/04/23,wkpi-street,23,MP,NORTHERN MARIANA ISLANDS,17 +320,pwwep-name,qtw-firstname,15160,utn-city,1958/06/08,sheh-street,176,FL,FLORIDA,20 +321,vkken-name,wrw-firstname,16760,jhm-city,1974/12/13,czmf-street,183,AR,ARKANSAS,20 +322,zbbmz-name,ipe-firstname,14340,mco-city,1970/04/25,ymou-street,2,WY,WYOMING,16 +323,eyzfr-name,xeb-firstname,10440,qsa-city,1990/04/15,iukl-street,135,AZ,ARIZONA,15 +324,tfpxy-name,yzh-firstname,14080,wbc-city,1970/02/12,ojfc-street,150,MP,NORTHERN MARIANA ISLANDS,17 +325,tphou-name,xac-firstname,15940,ncy-city,1962/09/22,vcxl-street,90,AL,ALABAMA,14 +326,bfscx-name,yih-firstname,18900,bxa-city,1962/10/12,qsww-street,137,VT,VERMONT,16 +327,ussbw-name,gbb-firstname,15600,wtg-city,1978/07/29,vqcq-street,10,KY,KENTUCKY,18 +328,zkoqj-name,mqn-firstname,13440,lux-city,1970/05/01,bokx-street,106,VA,VIRGINIA,20 +329,zbqew-name,dbw-firstname,14520,hbi-city,1971/01/20,syea-street,192,CT,CONNECTICUT,14 +330,vmfuy-name,qge-firstname,16660,vnk-city,1982/08/29,tlxy-street,166,PW,PALAU,16 +331,wajgr-name,mnx-firstname,17500,wiv-city,1954/07/10,ylug-street,21,OR,OREGON,12 +332,lzcry-name,tzk-firstname,18980,icz-city,1966/06/04,ldvu-street,25,GU,GUAM,19 +333,rodpv-name,rix-firstname,16540,tpf-city,1986/02/03,leur-street,169,SD,SOUTH DAKOTA,19 +334,pxmhq-name,tyd-firstname,11200,okj-city,1978/12/30,tauk-street,94,ND,NORTH DAKOTA,14 +335,kansg-name,qzs-firstname,18020,yhj-city,1982/08/27,ehie-street,140,AR,ARKANSAS,20 +336,kpqff-name,bqs-firstname,13780,tnp-city,1958/08/07,euhw-street,182,SD,SOUTH DAKOTA,19 +337,akvdd-name,bxi-firstname,15620,rbk-city,1962/03/16,fzdy-street,125,WA,WASHINGTON,18 +338,nbazl-name,ikv-firstname,13160,wci-city,1966/08/22,smzb-street,136,SD,SOUTH DAKOTA,19 +339,zrowi-name,udo-firstname,16460,hbe-city,1962/07/07,xgyd-street,170,AZ,ARIZONA,15 +340,jfzjd-name,atn-firstname,13040,yef-city,1974/09/26,dyjj-street,127,WI,WISCONSIN,17 +341,prsbo-name,ibh-firstname,19520,qro-city,1986/01/10,pemg-street,183,SC,SOUTH CAROLINA,8 +342,dwfdn-name,kci-firstname,14640,fze-city,1970/07/24,xxdz-street,102,IA,IOWA,14 +343,cnupm-name,rjl-firstname,14660,ewk-city,1966/07/02,wzjr-street,107,PW,PALAU,16 +344,wbpor-name,fuf-firstname,11300,xne-city,1978/02/14,tyzx-street,2,AL,ALABAMA,14 +345,tjqky-name,mjf-firstname,12560,qlo-city,1982/10/21,hzos-street,121,NE,NEBRASKA,20 +346,yxqsn-name,ofx-firstname,18680,ute-city,1974/02/27,wqka-street,111,FL,FLORIDA,20 +347,wjwmv-name,hra-firstname,14520,mvj-city,1954/10/03,chmz-street,70,SD,SOUTH DAKOTA,19 +348,aipnn-name,paa-firstname,14400,gyq-city,1970/04/27,tbpq-street,84,undefined,undefined,13 +349,wplyl-name,zvh-firstname,15820,vhw-city,1967/01/12,eesj-street,110,undefined,undefined,13 +350,syoip-name,lbd-firstname,13860,wfo-city,1966/03/17,lsvf-street,47,VI,VIRGIN ISLANDS,14 +351,lvhdk-name,pzl-firstname,14320,bab-city,1950/05/01,wktz-street,134,AR,ARKANSAS,20 +352,jodya-name,apd-firstname,14520,nfr-city,1986/12/14,jgzs-street,193,KY,KENTUCKY,18 +353,ikunj-name,syx-firstname,17320,cqd-city,1982/08/19,xwzj-street,53,IN,INDIANA,20 +354,bnhud-name,uvd-firstname,15360,ynw-city,1971/01/25,gens-street,107,MT,MONTANA,19 +355,prkrf-name,ivm-firstname,19220,kie-city,1966/11/14,boem-street,59,HI,HAWAII,22 +356,ucywi-name,sjl-firstname,11480,ish-city,1982/11/13,szck-street,148,NE,NEBRASKA,20 +357,sgftd-name,ajp-firstname,11760,jco-city,1958/03/05,djek-street,196,UT,UTAH,19 +358,kohdq-name,phr-firstname,13120,kpb-city,1978/07/26,csom-street,17,PR,PUERTO RICO,12 +359,fgjoa-name,srp-firstname,11480,onn-city,1990/05/26,duvo-street,127,MS,MISSISSIPPI,11 +360,xgiyw-name,rcu-firstname,15140,mti-city,1958/09/26,lego-street,125,MA,MASSACHUSETTS,18 +361,kgcui-name,grq-firstname,12640,tjn-city,1986/02/08,ntoq-street,6,CT,CONNECTICUT,14 +362,ljyxw-name,lho-firstname,19100,hmd-city,1962/03/09,mqus-street,12,CT,CONNECTICUT,14 +363,vnvwy-name,vmv-firstname,14360,cpu-city,1966/04/20,cfts-street,16,MI,MICHIGAN,11 +364,gedwu-name,ocl-firstname,11760,vaf-city,1970/04/22,exot-street,121,MT,MONTANA,19 +365,enknw-name,vjk-firstname,11100,bge-city,1954/07/06,inhb-street,95,AS,AMERICAN SAMOA,15 +366,qmpom-name,tmp-firstname,12260,ymn-city,1986/07/02,psdj-street,71,MI,MICHIGAN,11 +367,jrbwj-name,imr-firstname,11080,qsx-city,1966/11/08,msys-street,80,ID,IDAHO,14 +368,dzjpp-name,cwl-firstname,15720,ipm-city,1958/07/13,acsb-street,87,IL,ILLINOIS,13 +369,qdwdn-name,dtc-firstname,12740,qab-city,1986/11/29,besk-street,80,DC,DISTRICT OF COLUMBIA,16 +370,gbeyp-name,lzl-firstname,19740,ljz-city,1974/10/01,zwga-street,52,RI,RHODE ISLAND,20 +371,kuwva-name,noq-firstname,17500,xia-city,1950/03/18,omzn-street,164,KS,KANSAS,18 +372,jjhsm-name,cdc-firstname,13020,xli-city,1986/06/10,nups-street,38,VA,VIRGINIA,20 +373,xyupl-name,fyd-firstname,16100,fqd-city,1971/01/05,icjo-street,28,NE,NEBRASKA,20 +374,ueipj-name,meb-firstname,19880,nsk-city,1974/08/15,aqwr-street,14,LA,LOUISIANA,21 +375,vdmif-name,fat-firstname,19120,dud-city,1974/07/01,krgw-street,178,MS,MISSISSIPPI,11 +376,oxgdf-name,apn-firstname,11400,dji-city,1962/04/02,ttnt-street,13,HI,HAWAII,22 +377,xgxyc-name,jrn-firstname,19400,bfg-city,1950/12/30,jzgy-street,43,WY,WYOMING,16 +378,bczfk-name,bfu-firstname,16920,qxp-city,1974/05/26,seja-street,178,HI,HAWAII,22 +379,pppqe-name,kwo-firstname,15260,geb-city,1986/04/06,okvv-street,11,MN,MINNESOTA,23 +380,opzyy-name,mfk-firstname,14960,nlo-city,1962/03/03,dezo-street,106,AS,AMERICAN SAMOA,15 +381,xmnsm-name,ckj-firstname,10400,fnr-city,1950/10/21,uhme-street,154,NC,NORTH CAROLINA,16 +382,moqtn-name,zgw-firstname,12920,pqk-city,1950/10/28,suvi-street,102,WY,WYOMING,16 +383,byapu-name,pix-firstname,10900,gik-city,1982/09/04,ntiq-street,45,VI,VIRGIN ISLANDS,14 +384,zuhdb-name,gbj-firstname,18760,vkk-city,1978/06/17,vnem-street,62,TX,TEXAS,16 +385,gkjpo-name,qwq-firstname,12380,ame-city,1982/12/01,ndvp-street,94,VA,VIRGINIA,20 +386,yefwf-name,aev-firstname,13580,mor-city,1962/07/09,ldcg-street,91,AL,ALABAMA,14 +387,twgsp-name,mwx-firstname,15840,bbp-city,1970/05/12,hecl-street,137,WY,WYOMING,16 +388,zatdb-name,tes-firstname,17080,yga-city,1954/11/05,dfwu-street,58,undefined,undefined,13 +389,qxdyx-name,tum-firstname,18100,mqw-city,1958/10/21,gndl-street,156,FL,FLORIDA,20 +390,ncpki-name,wbm-firstname,13860,cuo-city,1970/07/02,yqpa-street,30,NE,NEBRASKA,20 +391,kyzsl-name,djj-firstname,17400,zzd-city,1978/02/11,mvkp-street,4,ME,MAINE,19 +392,jtksy-name,ayp-firstname,10760,gui-city,1982/12/17,wohf-street,175,DC,DISTRICT OF COLUMBIA,16 +393,grzby-name,oyz-firstname,15140,bmz-city,1974/07/10,rrui-street,60,NC,NORTH CAROLINA,16 +394,sgaut-name,xzd-firstname,15260,dnb-city,1978/10/01,hcii-street,169,RI,RHODE ISLAND,20 +395,ozkaq-name,brx-firstname,11400,guw-city,1962/02/08,pswz-street,194,NH,NEW HAMPSHIRE,14 +396,uqivg-name,map-firstname,10880,lgr-city,1990/07/14,lxmi-street,143,MN,MINNESOTA,23 +397,soqkg-name,jws-firstname,17200,dss-city,1970/12/05,ppht-street,187,UT,UTAH,19 +398,pdqdm-name,erh-firstname,11860,obj-city,1986/04/03,aova-street,121,CT,CONNECTICUT,14 +399,ogqrv-name,uyf-firstname,16400,abs-city,1987/01/06,xwue-street,114,AZ,ARIZONA,15 +400,ukcxw-name,ltd-firstname,11760,bwi-city,1986/09/01,ddjt-street,199,AR,ARKANSAS,20 +401,djcgr-name,tet-firstname,19380,ure-city,1962/09/03,alzp-street,169,SC,SOUTH CAROLINA,8 +402,ibbvs-name,cyv-firstname,16060,gdh-city,1982/04/24,qrdz-street,116,NV,NEVADA,24 +403,blmke-name,jtq-firstname,17260,tls-city,1970/05/28,eylx-street,171,NV,NEVADA,24 +404,qpatk-name,mtt-firstname,13400,nzv-city,1970/03/25,exby-street,93,WY,WYOMING,16 +405,mbngf-name,pqp-firstname,10720,ann-city,1966/09/01,rkpu-street,146,CT,CONNECTICUT,14 +406,rcydx-name,usf-firstname,12880,jou-city,1982/02/20,gtqw-street,186,MO,MISSOURI,19 +407,srszq-name,dtb-firstname,14340,yrs-city,1978/03/16,wtan-street,89,MN,MINNESOTA,23 +408,ezlfr-name,ebd-firstname,19420,ctw-city,1986/03/22,zjyv-street,44,VT,VERMONT,16 +409,bhotj-name,hzt-firstname,16480,rbt-city,1978/06/11,mbsm-street,157,WV,WEST VIRGINIA,22 +410,udzwf-name,kfd-firstname,13440,maj-city,1986/06/06,kerz-street,180,AZ,ARIZONA,15 +411,yflfv-name,zdl-firstname,13300,zix-city,1982/05/20,ozcp-street,153,MN,MINNESOTA,23 +412,wlyyq-name,kdz-firstname,11400,ygb-city,1966/11/07,zekv-street,111,AS,AMERICAN SAMOA,15 +413,wonzh-name,eeb-firstname,19920,qnt-city,1982/09/20,fxob-street,95,LA,LOUISIANA,21 +414,eiwzh-name,hpm-firstname,15860,bfo-city,1967/01/30,tkpg-street,182,VA,VIRGINIA,20 +415,mbqgk-name,jsm-firstname,15040,dai-city,1954/06/12,jqdh-street,65,NE,NEBRASKA,20 +416,jhigj-name,qyn-firstname,18360,ntm-city,1974/05/15,eghd-street,188,ME,MAINE,19 +417,llytl-name,jqe-firstname,11280,kkp-city,1974/04/03,oudg-street,69,DE,DELAWARE,18 +418,urhgl-name,iji-firstname,13760,lhf-city,1962/12/02,oywx-street,6,SC,SOUTH CAROLINA,8 +419,uflrm-name,hef-firstname,15040,usl-city,1974/11/06,qvgi-street,103,OK,OKLAHOMA,26 +420,gbfui-name,goz-firstname,14940,edu-city,1986/02/25,gkqy-street,26,KS,KANSAS,18 +421,nysbp-name,fro-firstname,18420,wqt-city,1958/12/19,vpoc-street,89,GA,GEORGIA,14 +422,fmrwo-name,wlf-firstname,17820,qwb-city,1962/05/03,stcz-street,22,CA,CALIFORNIA,19 +423,racwd-name,kqr-firstname,10180,xdr-city,1974/04/09,fqxz-street,15,MO,MISSOURI,19 +424,jpopz-name,krm-firstname,13420,fjx-city,1970/10/29,kyph-street,54,NJ,NEW JERSEY,17 +425,fwdat-name,ppn-firstname,15660,zqh-city,1986/06/24,zgfe-street,61,SC,SOUTH CAROLINA,8 +426,orznz-name,hyy-firstname,12020,lju-city,1982/07/01,xisy-street,125,GU,GUAM,19 +427,spzxo-name,mpv-firstname,13220,cbq-city,1962/05/09,qlqx-street,53,OK,OKLAHOMA,26 +428,qxdra-name,ifp-firstname,10480,nvu-city,1958/08/15,egsq-street,133,MH,MARSHALL ISLANDS,14 +429,yhelu-name,jsc-firstname,17200,clp-city,1954/12/01,vahx-street,3,NV,NEVADA,24 +430,umvfv-name,mbe-firstname,13600,knp-city,1954/07/16,oldf-street,188,PA,PENNSYLVANIA,15 +431,mwcfe-name,xxi-firstname,15080,chq-city,1974/12/22,kpsj-street,163,NV,NEVADA,24 +432,wvkrp-name,dtr-firstname,19840,pqv-city,1986/07/07,jnzd-street,119,HI,HAWAII,22 +433,vqfja-name,kep-firstname,19380,ydo-city,1966/11/11,gsfd-street,12,AR,ARKANSAS,20 +434,ikbjr-name,ipd-firstname,17920,uld-city,1990/03/03,tmbc-street,49,KS,KANSAS,18 +435,hyjrs-name,jqp-firstname,18820,vvm-city,1970/05/07,txye-street,152,AR,ARKANSAS,20 +436,tuewv-name,lkd-firstname,17060,slo-city,1970/07/06,htdm-street,197,OK,OKLAHOMA,26 +437,muyws-name,iql-firstname,19540,cwf-city,1962/04/27,knsx-street,167,LA,LOUISIANA,21 +438,tgsga-name,lww-firstname,17780,goh-city,1982/12/24,lzcl-street,136,SD,SOUTH DAKOTA,19 +439,agsyj-name,fve-firstname,19260,wgi-city,1970/03/07,aone-street,48,WV,WEST VIRGINIA,22 +440,yrgqp-name,tni-firstname,15820,mzp-city,1986/04/19,femc-street,29,PR,PUERTO RICO,12 +441,ltjmw-name,cps-firstname,13060,aeo-city,1954/07/17,bewv-street,182,OK,OKLAHOMA,26 +442,gjkpl-name,tre-firstname,16340,ndn-city,1962/08/27,ocld-street,16,WV,WEST VIRGINIA,22 +443,ryvgz-name,bhe-firstname,14960,lcg-city,1978/08/09,bxwa-street,19,NM,NEW MEXICO,10 +444,zgwwi-name,umb-firstname,11840,llj-city,1958/07/06,uiww-street,115,MP,NORTHERN MARIANA ISLANDS,17 +445,qaczz-name,qng-firstname,10900,umr-city,1970/01/17,mlsy-street,6,VA,VIRGINIA,20 +446,xcruf-name,vbp-firstname,17840,vzl-city,1982/06/17,oatg-street,110,MN,MINNESOTA,23 +447,egqdv-name,boy-firstname,18360,gfw-city,1958/10/07,qaoh-street,104,AS,AMERICAN SAMOA,15 +448,digvt-name,fid-firstname,11180,iyw-city,1958/03/13,pooo-street,119,NV,NEVADA,24 +449,gxavv-name,bal-firstname,17020,cra-city,1966/02/01,nchf-street,122,CO,COLORADO,18 +450,tiwoz-name,vwo-firstname,19340,oja-city,1982/07/16,bnsy-street,149,MA,MASSACHUSETTS,18 +451,jemtu-name,gnk-firstname,11180,gbb-city,1954/03/01,pmbh-street,87,NC,NORTH CAROLINA,16 +452,rhdjl-name,qaf-firstname,16500,nqr-city,1974/03/28,vmrd-street,75,DE,DELAWARE,18 +453,qdkbn-name,cpl-firstname,17460,ugb-city,1987/01/29,cwoa-street,184,ME,MAINE,19 +454,tuhon-name,bbg-firstname,10500,cer-city,1958/03/02,ttst-street,184,WA,WASHINGTON,18 +455,wluxg-name,xpv-firstname,18240,axq-city,1958/07/28,useg-street,140,NY,NEW YORK,21 +456,ojkgh-name,tzq-firstname,16000,guv-city,1990/11/25,hagy-street,198,MN,MINNESOTA,23 +457,arxsx-name,ana-firstname,18620,oxx-city,1986/04/22,hkdb-street,28,SD,SOUTH DAKOTA,19 +458,ujpiw-name,bty-firstname,14000,kiy-city,1978/08/10,bmgt-street,176,PW,PALAU,16 +459,sufuk-name,izq-firstname,17740,kfy-city,1986/10/29,xxcz-street,26,MD,MARYLAND,18 +460,nedie-name,ajz-firstname,11820,fnf-city,1970/04/08,ccnd-street,108,MT,MONTANA,19 +461,vbfwv-name,anp-firstname,19880,qag-city,1962/11/17,tbcc-street,23,NJ,NEW JERSEY,17 +462,bmrzz-name,yfe-firstname,11300,rgi-city,1970/10/01,xbjp-street,26,FM,FEDERATED STATES OF MICRONESIA,12 +463,jewvw-name,ymh-firstname,13100,kcv-city,1982/03/01,cjbt-street,4,KY,KENTUCKY,18 +464,kxxpm-name,has-firstname,18500,hlp-city,1954/04/03,qsmq-street,77,MD,MARYLAND,18 +465,qkjxa-name,gdq-firstname,12240,qtz-city,1954/11/25,mevz-street,12,NM,NEW MEXICO,10 +466,vmdwq-name,vjm-firstname,19980,rmz-city,1986/09/09,ifxg-street,139,NC,NORTH CAROLINA,16 +467,ssgil-name,lkd-firstname,14220,ndd-city,1963/01/19,rjln-street,195,MD,MARYLAND,18 +468,szbhe-name,pwi-firstname,10100,iij-city,1966/04/09,mojp-street,177,AK,ALASKA,19 +469,eswrp-name,lts-firstname,10080,cuk-city,1966/03/29,plor-street,139,VI,VIRGIN ISLANDS,14 +470,tioeh-name,qgc-firstname,11800,zre-city,1954/06/05,owaq-street,98,LA,LOUISIANA,21 +471,fkzpf-name,bse-firstname,19040,cor-city,1962/06/21,aamy-street,53,NY,NEW YORK,21 +472,kczhq-name,hde-firstname,16380,siz-city,1986/12/15,rawc-street,127,ND,NORTH DAKOTA,14 +473,gpwqf-name,pae-firstname,12820,rga-city,1958/12/19,djsk-street,131,WY,WYOMING,16 +474,yvgmq-name,hzp-firstname,19020,ioc-city,1966/03/22,zdbt-street,106,FM,FEDERATED STATES OF MICRONESIA,12 +475,abjqk-name,pdo-firstname,13040,vnj-city,1962/09/08,kvwv-street,145,OR,OREGON,12 +476,adppx-name,gmz-firstname,16560,pah-city,1962/03/14,ynqs-street,107,WY,WYOMING,16 +477,lxcrs-name,arg-firstname,12160,med-city,1990/03/14,wlag-street,141,MT,MONTANA,19 +478,mrkfp-name,jbm-firstname,19760,dhu-city,1970/06/12,idan-street,145,UT,UTAH,19 +479,zmkad-name,vns-firstname,10080,aoe-city,1955/01/25,qgbd-street,174,FL,FLORIDA,20 +480,vftgh-name,nxs-firstname,11580,igp-city,1954/05/17,fief-street,183,MT,MONTANA,19 +481,bnafy-name,geg-firstname,11160,sfp-city,1974/04/26,tpnq-street,194,DE,DELAWARE,18 +482,mwqpn-name,lbw-firstname,17660,oot-city,1974/04/09,qxgk-street,18,AK,ALASKA,19 +483,cijcf-name,uvd-firstname,17940,ioy-city,1982/02/07,gfiz-street,147,MN,MINNESOTA,23 +484,kwjhv-name,swd-firstname,12400,pue-city,1970/12/10,sall-street,104,MN,MINNESOTA,23 +485,mfdfy-name,hsy-firstname,18260,bzl-city,1982/09/08,hsyc-street,76,RI,RHODE ISLAND,20 +486,cdrmm-name,mxa-firstname,11520,rie-city,1962/12/21,dxed-street,112,LA,LOUISIANA,21 +487,flyur-name,mzm-firstname,12260,wyi-city,1978/07/07,xqoj-street,156,WV,WEST VIRGINIA,22 +488,hkwnf-name,obg-firstname,14520,bib-city,1967/01/20,mvee-street,115,undefined,undefined,13 +489,kawax-name,pyn-firstname,10520,zjh-city,1966/09/01,fsmz-street,87,DE,DELAWARE,18 +490,nnhzs-name,sfo-firstname,19040,thn-city,1990/11/08,tzsb-street,43,IN,INDIANA,20 +491,xivec-name,gzo-firstname,16820,aha-city,1978/10/19,iolt-street,36,HI,HAWAII,22 +492,hyiju-name,plw-firstname,18620,zzu-city,1982/07/13,tydq-street,112,NV,NEVADA,24 +493,leylb-name,fcv-firstname,15720,biw-city,1958/09/18,bnpf-street,52,MT,MONTANA,19 +494,pweci-name,hcu-firstname,19020,fzb-city,1950/10/28,spdv-street,131,WY,WYOMING,16 +495,ddulq-name,crh-firstname,11540,yrm-city,1974/07/31,opds-street,95,OH,OHIO,12 +496,fyrha-name,wea-firstname,16620,vfe-city,1990/12/07,ukki-street,48,GU,GUAM,19 +497,vuypf-name,ugz-firstname,13320,ixw-city,1970/07/09,aptu-street,60,HI,HAWAII,22 +498,wezwd-name,oae-firstname,11180,egb-city,1982/02/27,ldea-street,2,IL,ILLINOIS,13 +499,wrokv-name,zaa-firstname,13980,hac-city,1974/01/20,zwst-street,21,MO,MISSOURI,19 +500,wtapc-name,ciu-firstname,15360,uvh-city,1962/08/03,mddz-street,196,IA,IOWA,14 +501,hdtsu-name,two-firstname,19500,ick-city,1958/11/07,kipe-street,42,DE,DELAWARE,18 +502,lwprl-name,jaw-firstname,14140,acy-city,1970/12/06,jhae-street,129,MT,MONTANA,19 +503,iwftp-name,jnp-firstname,18800,axg-city,1978/10/13,rejy-street,174,AZ,ARIZONA,15 +504,qypws-name,fox-firstname,19820,bzu-city,1966/01/02,owsq-street,70,AR,ARKANSAS,20 +505,pzjcf-name,ese-firstname,12180,kjq-city,1958/11/21,xbyg-street,12,AS,AMERICAN SAMOA,15 +506,itzxd-name,erv-firstname,10460,dsk-city,1978/06/20,baci-street,151,OH,OHIO,12 +507,jnpdw-name,zna-firstname,11000,aqt-city,1966/05/27,pukm-street,80,WY,WYOMING,16 +508,dchwr-name,rxe-firstname,19220,plm-city,1958/05/18,gkgx-street,100,NH,NEW HAMPSHIRE,14 +509,pcszz-name,rym-firstname,15860,tml-city,1983/01/25,qrdz-street,7,RI,RHODE ISLAND,20 +510,fatdr-name,rcs-firstname,17480,ajx-city,1958/09/18,nlal-street,26,MD,MARYLAND,18 +511,oblzo-name,wwl-firstname,17280,hxs-city,1958/06/15,rnpa-street,20,AR,ARKANSAS,20 +512,nmflo-name,ljc-firstname,19720,biq-city,1962/03/23,ypux-street,197,OR,OREGON,12 +513,ajhdh-name,iba-firstname,16920,yru-city,1982/09/08,zedq-street,148,MN,MINNESOTA,23 +514,aiewz-name,gla-firstname,19340,zvj-city,1986/01/12,blie-street,116,MA,MASSACHUSETTS,18 +515,tglnr-name,fob-firstname,14300,ejm-city,1986/09/22,zazt-street,152,WV,WEST VIRGINIA,22 +516,tswnt-name,aal-firstname,19940,jsw-city,1978/07/02,xnjc-street,125,OK,OKLAHOMA,26 +517,smukz-name,zim-firstname,15260,pul-city,1974/09/26,furv-street,45,IA,IOWA,14 +518,aygln-name,qfk-firstname,16700,bmu-city,1958/03/18,esys-street,148,NE,NEBRASKA,20 +519,kcuub-name,ffc-firstname,10720,xnk-city,1958/02/23,cefn-street,135,AL,ALABAMA,14 +520,vsujm-name,yne-firstname,11280,gdr-city,1966/02/12,hdah-street,70,MS,MISSISSIPPI,11 +521,tirxh-name,gpy-firstname,19360,bai-city,1970/04/19,gznh-street,33,KY,KENTUCKY,18 +522,wlqnf-name,nnd-firstname,16120,kij-city,1954/10/07,gorj-street,85,ID,IDAHO,14 +523,rynel-name,iaq-firstname,13640,chy-city,1954/02/01,wbiu-street,62,WV,WEST VIRGINIA,22 +524,ohcvr-name,eod-firstname,18240,lcc-city,1978/12/24,guca-street,84,LA,LOUISIANA,21 +525,gabiw-name,gtj-firstname,17860,ezv-city,1978/01/18,jsyv-street,143,ND,NORTH DAKOTA,14 +526,ddajc-name,cab-firstname,12920,fgz-city,1958/10/06,lkvp-street,80,NV,NEVADA,24 +527,kivtx-name,pbs-firstname,17980,mso-city,1982/10/22,qden-street,69,IN,INDIANA,20 +528,tyial-name,mwb-firstname,17080,pdf-city,1954/02/25,qyym-street,48,MN,MINNESOTA,23 +529,ipbyo-name,lcr-firstname,12040,ygz-city,1978/03/01,qbqk-street,117,ND,NORTH DAKOTA,14 +530,tmkxn-name,jhl-firstname,13460,xkh-city,1966/06/07,wkzu-street,26,SD,SOUTH DAKOTA,19 +531,cnkac-name,jxe-firstname,13140,vgf-city,1974/08/24,sifo-street,84,ID,IDAHO,14 +532,zqnwe-name,ujv-firstname,19020,vjy-city,1966/02/03,sfzz-street,171,MN,MINNESOTA,23 +533,aphod-name,vsz-firstname,11440,spc-city,1962/07/28,alnl-street,152,FM,FEDERATED STATES OF MICRONESIA,12 +534,tjber-name,uhp-firstname,19320,quo-city,1958/12/19,zgus-street,173,FM,FEDERATED STATES OF MICRONESIA,12 +535,itblo-name,lbp-firstname,14840,rgn-city,1962/11/29,qmtb-street,1,IN,INDIANA,20 +536,navpk-name,wcs-firstname,14400,nvr-city,1982/11/01,qlar-street,126,AK,ALASKA,19 +537,nkyot-name,zep-firstname,17720,yim-city,1954/09/30,wwdl-street,198,ND,NORTH DAKOTA,14 +538,qebao-name,edw-firstname,19220,tlh-city,1982/10/16,celb-street,180,NJ,NEW JERSEY,17 +539,bzmvi-name,roq-firstname,10240,beg-city,1974/07/10,nnom-street,71,KY,KENTUCKY,18 +540,qtzxt-name,nau-firstname,17240,fis-city,1990/05/27,fbex-street,22,AR,ARKANSAS,20 +541,jarpd-name,mce-firstname,17060,rgw-city,1982/10/20,eqbd-street,180,WI,WISCONSIN,17 +542,ppaal-name,afq-firstname,17300,fno-city,1982/10/01,zhby-street,108,NY,NEW YORK,21 +543,sejhl-name,qwc-firstname,18000,sfv-city,1986/04/13,xhlx-street,52,CO,COLORADO,18 +544,qfvqk-name,mqn-firstname,10540,rwu-city,1958/07/30,xwsy-street,169,AL,ALABAMA,14 +545,ndowg-name,lnm-firstname,13820,llz-city,1954/08/11,niif-street,55,KS,KANSAS,18 +546,hutga-name,ztk-firstname,19800,uci-city,1950/10/04,ywtn-street,84,KS,KANSAS,18 +547,mudul-name,qcs-firstname,13520,agk-city,1962/04/20,scto-street,200,CO,COLORADO,18 +548,mnket-name,edm-firstname,10040,jqb-city,1954/11/09,avkk-street,23,VA,VIRGINIA,20 +549,mbxbn-name,yuu-firstname,18880,oho-city,1970/11/24,heug-street,116,PA,PENNSYLVANIA,15 +550,letcc-name,pyw-firstname,10160,lzb-city,1966/06/11,wtul-street,80,RI,RHODE ISLAND,20 +551,jqovo-name,eir-firstname,14120,bvr-city,1974/07/01,wdxz-street,156,SD,SOUTH DAKOTA,19 +552,jdkan-name,vxc-firstname,15600,rqa-city,1962/02/07,vroo-street,182,CO,COLORADO,18 +553,crzen-name,vwe-firstname,18800,msz-city,1962/10/17,xxbp-street,165,IA,IOWA,14 +554,nogoy-name,swt-firstname,11580,npe-city,1978/04/15,uath-street,55,NV,NEVADA,24 +555,qtkar-name,cdv-firstname,17020,opk-city,1950/07/26,bsnt-street,17,AK,ALASKA,19 +556,oigrk-name,zmz-firstname,14200,zhd-city,1966/11/30,yeqk-street,12,DC,DISTRICT OF COLUMBIA,16 +557,qspmb-name,htl-firstname,10940,oxl-city,1986/11/17,bqdp-street,71,NH,NEW HAMPSHIRE,14 +558,qktka-name,ces-firstname,12840,xcq-city,1986/02/13,ddqq-street,62,NE,NEBRASKA,20 +559,bokzl-name,vfw-firstname,14160,eyd-city,1970/02/15,ffrm-street,26,IL,ILLINOIS,13 +560,eksje-name,xxw-firstname,18600,vtn-city,1962/12/09,nskq-street,42,VA,VIRGINIA,20 +561,avxuw-name,niq-firstname,13520,cyx-city,1958/05/29,dxit-street,167,CA,CALIFORNIA,19 +562,xwskh-name,mud-firstname,10900,bcd-city,1954/02/15,lqcd-street,5,VA,VIRGINIA,20 +563,jrlgi-name,qbl-firstname,11780,xkb-city,1970/01/27,wfti-street,101,CO,COLORADO,18 +564,vvgef-name,qbi-firstname,17580,dgu-city,1974/10/21,pkhw-street,140,DE,DELAWARE,18 +565,vbdsa-name,mqy-firstname,19520,iqf-city,1958/06/24,gnhg-street,23,AL,ALABAMA,14 +566,mimxv-name,idp-firstname,19280,acp-city,1986/02/10,wziw-street,1,LA,LOUISIANA,21 +567,ihvtb-name,jdf-firstname,17540,xpg-city,1970/11/23,pjvm-street,156,SC,SOUTH CAROLINA,8 +568,wztgo-name,njm-firstname,15200,btu-city,1978/10/30,ihdb-street,25,OH,OHIO,12 +569,unnnh-name,zry-firstname,12300,upy-city,1982/09/15,nbjp-street,91,GU,GUAM,19 +570,qtsep-name,spo-firstname,15940,ryi-city,1983/01/26,kvcq-street,94,FM,FEDERATED STATES OF MICRONESIA,12 +571,rznvs-name,cjh-firstname,15180,duz-city,1974/05/12,igjl-street,111,AL,ALABAMA,14 +572,yrdmb-name,lvl-firstname,12240,pzi-city,1962/11/10,kegb-street,54,NY,NEW YORK,21 +573,jdhso-name,hms-firstname,12620,ova-city,1950/02/14,rxks-street,115,MS,MISSISSIPPI,11 +574,seigu-name,rwp-firstname,15620,yrt-city,1958/09/30,mloc-street,9,NJ,NEW JERSEY,17 +575,wzura-name,fzo-firstname,12500,vgt-city,1974/06/01,tevl-street,50,NC,NORTH CAROLINA,16 +576,urqhu-name,hiq-firstname,15020,ejs-city,1966/04/08,khev-street,38,AK,ALASKA,19 +577,yazce-name,zhy-firstname,12920,ytz-city,1982/05/07,rfsh-street,125,NH,NEW HAMPSHIRE,14 +578,okxei-name,qbi-firstname,17400,plc-city,1966/04/05,mvzs-street,73,KS,KANSAS,18 +579,eemhh-name,nsj-firstname,11660,buq-city,1986/06/23,mftt-street,191,NE,NEBRASKA,20 +580,uyoei-name,wiz-firstname,19000,lqo-city,1974/06/18,rvat-street,197,NC,NORTH CAROLINA,16 +581,ahbez-name,gwg-firstname,10940,gno-city,1978/03/23,rfwr-street,105,KS,KANSAS,18 +582,qnopb-name,xgc-firstname,18800,ayx-city,1962/05/30,lrnc-street,193,TN,TENNESSEE,11 +583,beqpz-name,psc-firstname,10880,sfe-city,1986/07/04,tobm-street,121,AK,ALASKA,19 +584,nnvir-name,tbu-firstname,12860,cuj-city,1962/04/25,wuxg-street,11,WA,WASHINGTON,18 +585,cmqrt-name,huk-firstname,19580,kae-city,1958/09/29,yukq-street,146,IA,IOWA,14 +586,ifivh-name,bub-firstname,15640,pti-city,1950/04/21,alaw-street,104,VT,VERMONT,16 +587,nqbqs-name,ndv-firstname,17500,sxr-city,1963/01/05,zclw-street,77,MP,NORTHERN MARIANA ISLANDS,17 +588,kgrlf-name,suz-firstname,16080,ayf-city,1990/03/07,vmto-street,120,CO,COLORADO,18 +589,nhjsx-name,xuh-firstname,12360,tqo-city,1970/08/04,vfzt-street,181,undefined,undefined,13 +590,vdint-name,zal-firstname,14600,fjh-city,1982/04/22,uopg-street,117,WV,WEST VIRGINIA,22 +591,nchqr-name,ldr-firstname,14700,pyy-city,1966/03/02,nlzm-street,137,VT,VERMONT,16 +592,sjmbq-name,tsc-firstname,19960,dlp-city,1974/05/26,qphd-street,155,IN,INDIANA,20 +593,njbsk-name,oft-firstname,17280,xtc-city,1958/02/13,sebl-street,85,HI,HAWAII,22 +594,vtijb-name,obv-firstname,14000,bpn-city,1962/08/08,mxrl-street,199,IL,ILLINOIS,13 +595,ihqjn-name,hnu-firstname,18960,ztv-city,1982/02/20,mhrl-street,45,MA,MASSACHUSETTS,18 +596,kpzko-name,zfz-firstname,18720,rjc-city,1971/01/01,ycrf-street,173,CT,CONNECTICUT,14 +597,qlfgm-name,fnv-firstname,11360,avw-city,1970/12/01,yvgg-street,49,LA,LOUISIANA,21 +598,oeepm-name,asq-firstname,18660,mvq-city,1990/07/29,ssey-street,57,OH,OHIO,12 +599,waklo-name,ksz-firstname,10660,ddf-city,1958/07/10,dilr-street,145,NY,NEW YORK,21 +600,mdftt-name,ldq-firstname,11460,mkm-city,1966/09/03,rjkt-street,9,CA,CALIFORNIA,19 +601,iowsr-name,vmg-firstname,18420,kie-city,1954/12/30,qfom-street,117,NE,NEBRASKA,20 +602,sdtxj-name,zbp-firstname,16160,gic-city,1962/09/09,eavm-street,108,IA,IOWA,14 +603,ocebd-name,img-firstname,12180,rxp-city,1966/08/18,ygmy-street,52,PA,PENNSYLVANIA,15 +604,uvovz-name,nsd-firstname,13600,tlc-city,1950/08/13,xbsm-street,110,MP,NORTHERN MARIANA ISLANDS,17 +605,fzuuf-name,pub-firstname,10500,ikg-city,1974/04/26,pmnl-street,189,MA,MASSACHUSETTS,18 +606,xzvbs-name,qpb-firstname,17060,xed-city,1950/04/18,vvdx-street,118,OR,OREGON,12 +607,qvkir-name,hns-firstname,12840,kxi-city,1978/03/02,kzbt-street,65,RI,RHODE ISLAND,20 +608,llarb-name,psp-firstname,11240,wlq-city,1990/10/10,qshw-street,106,AL,ALABAMA,14 +609,wrzts-name,ygm-firstname,15520,ybe-city,1974/12/03,uxct-street,200,ND,NORTH DAKOTA,14 +610,qfpdp-name,luy-firstname,16260,qma-city,1978/07/12,opsm-street,194,RI,RHODE ISLAND,20 +611,xaosj-name,kfd-firstname,18100,xmf-city,1982/12/22,hceh-street,75,LA,LOUISIANA,21 +612,iuocr-name,ztn-firstname,19000,sby-city,1970/06/07,qres-street,86,TX,TEXAS,16 +613,ocvrm-name,ylm-firstname,16360,vif-city,1986/11/17,zgov-street,17,CT,CONNECTICUT,14 +614,tealr-name,lbu-firstname,10020,blg-city,1958/03/23,qjvy-street,182,MS,MISSISSIPPI,11 +615,ksjja-name,dky-firstname,15360,qim-city,1986/08/30,rvtm-street,182,KY,KENTUCKY,18 +616,jbhpa-name,shc-firstname,18020,gmc-city,1974/05/29,cvpl-street,154,AS,AMERICAN SAMOA,15 +617,vraon-name,nam-firstname,16320,joa-city,1986/11/07,pnik-street,194,NM,NEW MEXICO,10 +618,gtymv-name,lgo-firstname,16740,ynr-city,1970/05/14,xxzj-street,74,NJ,NEW JERSEY,17 +619,sxbpo-name,hyx-firstname,12900,lvu-city,1958/07/14,tysz-street,68,HI,HAWAII,22 +620,yaogj-name,fjg-firstname,11040,slr-city,1974/06/22,kjoi-street,134,OK,OKLAHOMA,26 +621,rprrj-name,vca-firstname,19160,txr-city,1959/01/18,stjh-street,137,MA,MASSACHUSETTS,18 +622,dbaaq-name,jdi-firstname,13800,uge-city,1954/11/08,ftck-street,175,OK,OKLAHOMA,26 +623,eapsq-name,zeu-firstname,14720,mmh-city,1954/11/01,tjlj-street,84,AS,AMERICAN SAMOA,15 +624,wlrqb-name,srf-firstname,15280,bbq-city,1970/04/06,thvb-street,17,CO,COLORADO,18 +625,dzzia-name,ukn-firstname,14680,zug-city,1954/04/29,nthu-street,109,LA,LOUISIANA,21 +626,nfzxa-name,ikc-firstname,17080,sqt-city,1982/01/21,dteu-street,113,CT,CONNECTICUT,14 +627,ovxiz-name,lho-firstname,10620,gox-city,1954/07/05,cofd-street,155,NY,NEW YORK,21 +628,msubu-name,ccj-firstname,17780,jkv-city,1982/02/25,gfve-street,43,NV,NEVADA,24 +629,xxbdr-name,kas-firstname,17320,ick-city,1970/02/25,gvoi-street,24,ID,IDAHO,14 +630,rgryb-name,guv-firstname,11160,xus-city,1966/05/06,plka-street,164,FL,FLORIDA,20 +631,dxprz-name,byy-firstname,16520,lpb-city,1974/03/01,auoq-street,103,MH,MARSHALL ISLANDS,14 +632,nerdb-name,eqa-firstname,12860,arg-city,1958/03/17,emcz-street,104,DC,DISTRICT OF COLUMBIA,16 +633,nszhh-name,nrn-firstname,16860,hap-city,1974/04/30,zjuq-street,78,IA,IOWA,14 +634,jtcxq-name,hxi-firstname,12160,oqq-city,1982/06/03,xpsp-street,30,VI,VIRGIN ISLANDS,14 +635,prglc-name,kqd-firstname,16080,gqr-city,1978/10/14,pjin-street,145,NH,NEW HAMPSHIRE,14 +636,yofsk-name,qch-firstname,14240,wbz-city,1990/05/28,edsh-street,5,MI,MICHIGAN,11 +637,cupcm-name,jvy-firstname,13720,lkw-city,1966/10/14,frzx-street,83,NY,NEW YORK,21 +638,crjjf-name,yfe-firstname,19880,oof-city,1970/02/07,akkj-street,98,NC,NORTH CAROLINA,16 +639,rqyyg-name,hbx-firstname,12080,sxh-city,1958/04/24,wzjb-street,159,TX,TEXAS,16 +640,xxugu-name,yqf-firstname,10380,ocx-city,1954/11/18,mbps-street,20,ND,NORTH DAKOTA,14 +641,ayvhk-name,znx-firstname,16960,qbw-city,1954/07/23,wgsh-street,148,OK,OKLAHOMA,26 +642,xwdxq-name,aik-firstname,14280,djc-city,1958/06/18,mjpa-street,130,OH,OHIO,12 +643,qsvmf-name,lgn-firstname,17140,mpl-city,1982/06/15,ngot-street,193,NJ,NEW JERSEY,17 +644,tlued-name,mcz-firstname,16780,oab-city,1978/09/22,ppzd-street,111,AR,ARKANSAS,20 +645,ibhei-name,djd-firstname,11340,eep-city,1975/01/24,rzdt-street,94,MH,MARSHALL ISLANDS,14 +646,snfrd-name,jof-firstname,12700,lck-city,1978/12/16,jpcu-street,184,IN,INDIANA,20 +647,vkwed-name,elj-firstname,18940,tdi-city,1990/08/05,ekyx-street,137,KY,KENTUCKY,18 +648,acing-name,rxv-firstname,10660,gtt-city,1970/11/17,ltxb-street,117,VA,VIRGINIA,20 +649,fzmkk-name,ilb-firstname,18540,rwj-city,1986/02/08,xsnm-street,113,MA,MASSACHUSETTS,18 +650,knzxd-name,seg-firstname,17180,oqu-city,1982/11/02,ukda-street,80,MT,MONTANA,19 +651,zzyix-name,dlk-firstname,14180,yoc-city,1986/10/06,jnpv-street,71,FM,FEDERATED STATES OF MICRONESIA,12 +652,nsote-name,ivj-firstname,19740,aqh-city,1990/11/05,faox-street,77,WI,WISCONSIN,17 +653,znees-name,edg-firstname,18120,qkr-city,1986/12/08,prpo-street,42,DE,DELAWARE,18 +654,svzbm-name,hsg-firstname,19180,shh-city,1954/02/05,uglh-street,150,IA,IOWA,14 +655,llfur-name,geu-firstname,12340,qdf-city,1970/12/01,nntt-street,92,AZ,ARIZONA,15 +656,pusrl-name,ovn-firstname,17020,oaa-city,1954/07/30,oxfp-street,104,AL,ALABAMA,14 +657,wihqb-name,bkq-firstname,13540,jau-city,1974/09/25,vuga-street,83,NY,NEW YORK,21 +658,ezgdt-name,yaa-firstname,13380,fdc-city,1986/11/04,yxut-street,75,VI,VIRGIN ISLANDS,14 +659,kvtlv-name,zdr-firstname,15700,bqd-city,1990/06/04,kajd-street,92,DE,DELAWARE,18 +660,bqkzl-name,tym-firstname,14900,rhk-city,1962/02/13,yyzk-street,165,MH,MARSHALL ISLANDS,14 +661,ndfve-name,ajt-firstname,19460,tdr-city,1970/02/04,vpub-street,143,CT,CONNECTICUT,14 +662,temdh-name,trc-firstname,11720,ald-city,1950/05/06,tapb-street,1,FL,FLORIDA,20 +663,bzagi-name,dwa-firstname,13580,zvq-city,1970/05/05,khyi-street,105,CT,CONNECTICUT,14 +664,frtvn-name,ukn-firstname,19400,eto-city,1986/06/29,dcnz-street,114,MH,MARSHALL ISLANDS,14 +665,mjdqu-name,xlw-firstname,14420,lpe-city,1962/08/02,oecb-street,189,IN,INDIANA,20 +666,fnopj-name,sdh-firstname,19080,hxm-city,1974/11/24,pfxz-street,66,ME,MAINE,19 +667,kuxaz-name,hye-firstname,14860,eia-city,1962/09/16,nhdg-street,198,CO,COLORADO,18 +668,tfrpe-name,mme-firstname,12860,mox-city,1978/03/24,uhqo-street,10,NE,NEBRASKA,20 +669,eodve-name,tzj-firstname,11440,syb-city,1986/07/29,jlnw-street,74,WI,WISCONSIN,17 +670,slzsi-name,nvw-firstname,13680,kgz-city,1958/07/13,pptx-street,88,NY,NEW YORK,21 +671,wrttz-name,wwx-firstname,13440,lig-city,1986/11/05,vdph-street,66,MI,MICHIGAN,11 +672,wfxhy-name,wtc-firstname,12760,gnf-city,1970/08/22,itut-street,3,HI,HAWAII,22 +673,toeja-name,vrw-firstname,18900,xes-city,1982/09/01,wpqu-street,184,undefined,undefined,13 +674,glmnt-name,bck-firstname,11680,cuu-city,1978/08/02,wtxv-street,16,RI,RHODE ISLAND,20 +675,vyvnk-name,shl-firstname,10260,xhq-city,1970/06/06,ddbz-street,28,MA,MASSACHUSETTS,18 +676,pjdvs-name,adv-firstname,15600,snf-city,1986/12/10,tdft-street,133,DC,DISTRICT OF COLUMBIA,16 +677,rulrt-name,ytn-firstname,19340,spb-city,1974/11/11,bpwx-street,24,VA,VIRGINIA,20 +678,gvttx-name,moj-firstname,17000,gri-city,1978/11/24,kqlt-street,27,ID,IDAHO,14 +679,jtlah-name,gxq-firstname,15440,jte-city,1978/02/01,mbvv-street,172,OH,OHIO,12 +680,cpvhs-name,ruo-firstname,16080,vhq-city,1970/10/18,ditw-street,164,AR,ARKANSAS,20 +681,jikyx-name,hzl-firstname,13320,hgh-city,1958/08/07,gmqp-street,85,FL,FLORIDA,20 +682,bsvsn-name,idb-firstname,15780,ooc-city,1958/04/28,apff-street,78,MO,MISSOURI,19 +683,qgivn-name,bau-firstname,11580,uez-city,1987/01/29,vpsa-street,48,RI,RHODE ISLAND,20 +684,hqbfl-name,fgr-firstname,11620,ejc-city,1982/02/07,fwsq-street,107,WA,WASHINGTON,18 +685,crmtk-name,fmd-firstname,14480,mdp-city,1962/05/23,knxw-street,18,IL,ILLINOIS,13 +686,kccgj-name,pzd-firstname,10760,ifv-city,1990/05/14,dgfl-street,172,PR,PUERTO RICO,12 +687,mddbw-name,lcu-firstname,11360,crz-city,1970/08/24,eiwf-street,32,PR,PUERTO RICO,12 +688,dlnxj-name,tzx-firstname,11380,nza-city,1966/09/25,jlna-street,186,MO,MISSOURI,19 +689,wpjcx-name,hwx-firstname,13100,slr-city,1987/01/16,wxcr-street,42,AK,ALASKA,19 +690,yefnh-name,jzp-firstname,13080,ozf-city,1986/08/09,zzos-street,120,OR,OREGON,12 +691,qsoei-name,kio-firstname,11860,esa-city,1974/08/04,bhbl-street,86,HI,HAWAII,22 +692,xwodh-name,aid-firstname,17940,wgd-city,1954/07/21,rcrf-street,90,IL,ILLINOIS,13 +693,oncpb-name,elk-firstname,12020,rbp-city,1974/10/28,iblz-street,114,DE,DELAWARE,18 +694,qisar-name,kfc-firstname,19800,alw-city,1958/08/07,ukbj-street,53,NH,NEW HAMPSHIRE,14 +695,bvsga-name,qln-firstname,18060,zlt-city,1966/01/29,rngf-street,46,WA,WASHINGTON,18 +696,xguku-name,uxf-firstname,10080,ezg-city,1974/12/30,icvq-street,48,UT,UTAH,19 +697,hxylt-name,hng-firstname,15360,jgc-city,1966/03/11,lhxd-street,87,DE,DELAWARE,18 +698,dmlyv-name,qai-firstname,20000,hoe-city,1966/08/20,uboj-street,72,KY,KENTUCKY,18 +699,kkbjx-name,cnx-firstname,16780,ndu-city,1970/08/16,afzw-street,55,NJ,NEW JERSEY,17 +700,emoio-name,awr-firstname,18560,qpj-city,1974/11/17,hasx-street,92,VT,VERMONT,16 +701,lxpni-name,oox-firstname,14100,xrj-city,1986/06/09,wvvi-street,106,MO,MISSOURI,19 +702,oshuu-name,mim-firstname,14280,mns-city,1982/09/12,ashj-street,95,FM,FEDERATED STATES OF MICRONESIA,12 +703,uhpxg-name,gnt-firstname,19180,zsi-city,1978/05/22,namc-street,126,AL,ALABAMA,14 +704,lvoek-name,reg-firstname,18920,ubj-city,1954/12/24,nrjw-street,38,PA,PENNSYLVANIA,15 +705,lpnhx-name,szp-firstname,10720,ybd-city,1970/11/26,ntyc-street,109,GA,GEORGIA,14 +706,kppvg-name,ztz-firstname,14660,ocp-city,1990/08/19,ekcr-street,56,SD,SOUTH DAKOTA,19 +707,vxwxv-name,qzo-firstname,15540,rfl-city,1962/03/29,hcwy-street,116,KY,KENTUCKY,18 +708,nbgan-name,ocf-firstname,15240,aas-city,1954/09/14,zkkh-street,117,IN,INDIANA,20 +709,faiaw-name,lup-firstname,16980,rsz-city,1986/09/17,yddi-street,98,IN,INDIANA,20 +710,mdmax-name,ggz-firstname,17360,stn-city,1966/11/25,lwhf-street,198,IN,INDIANA,20 +711,etauo-name,pta-firstname,13360,fnh-city,1978/02/03,gsrn-street,113,IA,IOWA,14 +712,gckpz-name,rou-firstname,17920,liu-city,1974/10/10,apjm-street,40,AZ,ARIZONA,15 +713,szvhz-name,ani-firstname,17240,nab-city,1958/12/10,cxho-street,87,KY,KENTUCKY,18 +714,dawkl-name,epe-firstname,11780,pjg-city,1990/04/24,hdbx-street,104,WI,WISCONSIN,17 +715,eifes-name,zka-firstname,11500,uux-city,1986/10/20,pazo-street,125,TN,TENNESSEE,11 +716,fphzw-name,gof-firstname,14680,dkz-city,1986/06/23,aejx-street,88,VA,VIRGINIA,20 +717,bgicm-name,one-firstname,14820,lwh-city,1966/01/08,dizm-street,53,AR,ARKANSAS,20 +718,qnozr-name,fwa-firstname,17800,rsm-city,1954/04/11,tuun-street,63,NE,NEBRASKA,20 +719,zxcjb-name,mhs-firstname,18620,czu-city,1982/11/15,ucfg-street,103,PW,PALAU,16 +720,qqftw-name,dox-firstname,11280,eii-city,1982/08/03,akjj-street,174,VT,VERMONT,16 +721,byevj-name,bah-firstname,14240,rge-city,1958/01/21,rkkm-street,142,MD,MARYLAND,18 +722,ihvvb-name,gaq-firstname,19140,gua-city,1958/09/30,hxim-street,88,RI,RHODE ISLAND,20 +723,suiee-name,vji-firstname,12120,zuz-city,1970/02/22,ijcn-street,84,PW,PALAU,16 +724,fgkoi-name,xkx-firstname,15880,zuk-city,1970/12/27,frfg-street,74,IL,ILLINOIS,13 +725,aowhf-name,hvv-firstname,12740,mpj-city,1987/01/06,mrmx-street,95,KY,KENTUCKY,18 +726,vxkbl-name,xfk-firstname,18600,odo-city,1970/02/28,jrzc-street,128,AZ,ARIZONA,15 +727,zpedp-name,oxh-firstname,16380,cjz-city,1958/05/28,bnxj-street,18,GU,GUAM,19 +728,uqfju-name,dmq-firstname,15560,ehf-city,1954/03/02,ptqn-street,68,VA,VIRGINIA,20 +729,wsoxm-name,xtd-firstname,15660,ogk-city,1951/01/29,inkh-street,150,WV,WEST VIRGINIA,22 +730,cvypk-name,xoy-firstname,15080,cwr-city,1970/02/28,kyio-street,117,WV,WEST VIRGINIA,22 +731,yprgp-name,vgc-firstname,14660,alm-city,1971/01/05,uoxp-street,110,CA,CALIFORNIA,19 +732,udhil-name,dsa-firstname,12060,lzv-city,1974/02/18,gvvc-street,149,AK,ALASKA,19 +733,fgtjr-name,bnt-firstname,18240,pgf-city,1986/10/15,synp-street,159,CT,CONNECTICUT,14 +734,hypgs-name,ycz-firstname,10840,ufx-city,1966/06/26,frur-street,81,HI,HAWAII,22 +735,ijqas-name,bgl-firstname,18680,dss-city,1950/02/20,rafi-street,134,ID,IDAHO,14 +736,seeae-name,pvg-firstname,10800,txd-city,1958/10/18,uibx-street,102,GU,GUAM,19 +737,punzk-name,ubx-firstname,15020,qzq-city,1978/07/13,cqww-street,52,PA,PENNSYLVANIA,15 +738,xyvjy-name,jta-firstname,19700,koe-city,1986/06/22,lrjw-street,17,undefined,undefined,13 +739,ysnek-name,kop-firstname,13120,bel-city,1950/08/01,xcbk-street,94,MA,MASSACHUSETTS,18 +740,ozxng-name,sje-firstname,15180,nqf-city,1986/11/19,uivr-street,117,MP,NORTHERN MARIANA ISLANDS,17 +741,ekqcg-name,hxq-firstname,16880,jic-city,1966/01/11,grbw-street,86,UT,UTAH,19 +742,xxhts-name,krw-firstname,14140,lwz-city,1978/04/06,fzbe-street,40,WI,WISCONSIN,17 +743,uffha-name,jdu-firstname,13080,jmd-city,1986/06/21,sqbk-street,66,KS,KANSAS,18 +744,aevbo-name,bba-firstname,16880,hgh-city,1958/03/08,urgb-street,74,NE,NEBRASKA,20 +745,rqwnb-name,zpb-firstname,19300,cbx-city,1978/08/12,qavq-street,5,IN,INDIANA,20 +746,txozw-name,vet-firstname,15540,ajt-city,1954/07/05,jayh-street,105,RI,RHODE ISLAND,20 +747,xmyzv-name,phl-firstname,15220,jnk-city,1974/04/04,ckwm-street,174,KY,KENTUCKY,18 +748,fusnk-name,iva-firstname,14660,kqd-city,1982/07/03,zmbq-street,10,FL,FLORIDA,20 +749,ksnje-name,eaq-firstname,12500,gej-city,1982/10/03,vbwg-street,83,HI,HAWAII,22 +750,kcahb-name,srs-firstname,11300,bvz-city,1978/05/16,gyfr-street,164,LA,LOUISIANA,21 +751,bzjht-name,vfn-firstname,17500,mgi-city,1958/06/15,mmko-street,172,DE,DELAWARE,18 +752,loydq-name,gft-firstname,11900,fsr-city,1990/01/20,sjds-street,62,TN,TENNESSEE,11 +753,kglxv-name,zlt-firstname,14960,qgb-city,1982/02/07,gniz-street,178,AS,AMERICAN SAMOA,15 +754,loimm-name,ipg-firstname,15980,sxt-city,1978/10/31,kdlk-street,22,NY,NEW YORK,21 +755,demtf-name,yhq-firstname,12300,pdo-city,1982/09/18,mfye-street,99,TN,TENNESSEE,11 +756,jdmdi-name,llu-firstname,18060,pss-city,1974/07/01,utrs-street,198,CA,CALIFORNIA,19 +757,crjfv-name,ccn-firstname,19520,uub-city,1978/07/12,xtvk-street,138,OK,OKLAHOMA,26 +758,irghm-name,dbi-firstname,15200,otd-city,1982/12/14,iple-street,144,WY,WYOMING,16 +759,mcwlu-name,kxy-firstname,17640,edu-city,1958/07/05,ibtg-street,173,IN,INDIANA,20 +760,plfvg-name,cxa-firstname,19360,rnc-city,1958/04/06,zemc-street,64,DE,DELAWARE,18 +761,dnxai-name,gnd-firstname,11580,ers-city,1966/07/14,ytjt-street,163,OH,OHIO,12 +762,ksbwq-name,tku-firstname,10040,axu-city,1974/08/10,kvel-street,95,RI,RHODE ISLAND,20 +763,durgp-name,unr-firstname,12820,juk-city,1959/01/05,ucky-street,132,HI,HAWAII,22 +764,eocqj-name,qcg-firstname,18940,faf-city,1986/10/14,gjjy-street,126,OK,OKLAHOMA,26 +765,dhcim-name,eti-firstname,17140,aed-city,1982/07/19,syxk-street,156,FL,FLORIDA,20 +766,nerhu-name,jfx-firstname,10140,huk-city,1978/02/07,cyfs-street,47,ME,MAINE,19 +767,sxukr-name,hag-firstname,14400,lfc-city,1963/01/05,wylb-street,38,WV,WEST VIRGINIA,22 +768,xznte-name,snc-firstname,17840,mhl-city,1962/03/23,rfig-street,151,DE,DELAWARE,18 +769,tgwig-name,rfn-firstname,14020,fcs-city,1986/11/12,agpo-street,79,undefined,undefined,13 +770,kmkbd-name,fzv-firstname,12920,kjt-city,1974/03/03,sgjo-street,63,MN,MINNESOTA,23 +771,kqzne-name,gvz-firstname,15500,zar-city,1966/04/01,kvlk-street,35,DC,DISTRICT OF COLUMBIA,16 +772,hatuj-name,fwt-firstname,16040,gih-city,1978/07/10,wbjj-street,70,MD,MARYLAND,18 +773,anvjr-name,qqu-firstname,15280,php-city,1970/09/28,hhnz-street,147,GU,GUAM,19 +774,jxiox-name,ein-firstname,18560,vou-city,1974/04/29,dlcm-street,52,VA,VIRGINIA,20 +775,ahdci-name,fhq-firstname,10040,dis-city,1974/05/17,flyu-street,84,MO,MISSOURI,19 +776,slvkr-name,qtq-firstname,10800,seu-city,1978/08/24,hivm-street,148,PR,PUERTO RICO,12 +777,xncyt-name,alj-firstname,12500,qcq-city,1962/05/20,bqjo-street,78,VI,VIRGIN ISLANDS,14 +778,oagkd-name,oud-firstname,10720,nai-city,1974/09/12,jbps-street,50,MT,MONTANA,19 +779,bhinx-name,sde-firstname,18240,zrt-city,1962/07/08,dxuq-street,97,PA,PENNSYLVANIA,15 +780,wpyvx-name,qwg-firstname,18520,lku-city,1966/03/09,ymkt-street,133,MD,MARYLAND,18 +781,wvkan-name,ndd-firstname,10340,hwn-city,1978/11/18,pqqf-street,114,NV,NEVADA,24 +782,biapu-name,kiq-firstname,16360,lbi-city,1954/10/27,pawe-street,198,OK,OKLAHOMA,26 +783,ohygc-name,vxw-firstname,11100,flm-city,1966/11/20,homq-street,170,HI,HAWAII,22 +784,ntbgw-name,msg-firstname,13740,cth-city,1958/09/28,epdz-street,80,AS,AMERICAN SAMOA,15 +785,vegli-name,avd-firstname,18000,gob-city,1970/08/27,bvnt-street,32,WY,WYOMING,16 +786,aadoy-name,gre-firstname,16040,qln-city,1962/01/03,lqxs-street,179,ME,MAINE,19 +787,rzwid-name,xjl-firstname,16300,eds-city,1970/05/08,hutz-street,27,TX,TEXAS,16 +788,qperl-name,nsk-firstname,13920,hxo-city,1966/11/16,xvzr-street,160,IA,IOWA,14 +789,hkhzc-name,hxd-firstname,19980,gpm-city,1954/08/23,qmrm-street,60,VA,VIRGINIA,20 +790,qlixr-name,tsd-firstname,16780,dvv-city,1978/02/14,ibwa-street,85,AZ,ARIZONA,15 +791,ieyhf-name,oox-firstname,19820,mfp-city,1958/02/17,ntdb-street,100,MD,MARYLAND,18 +792,xxtwm-name,zpp-firstname,12980,udl-city,1970/03/01,dkzb-street,157,DC,DISTRICT OF COLUMBIA,16 +793,hvtaa-name,wcr-firstname,14460,zek-city,1970/01/18,bayt-street,149,PR,PUERTO RICO,12 +794,xduae-name,iqk-firstname,18380,lkm-city,1982/11/24,aujb-street,62,GA,GEORGIA,14 +795,qkjcz-name,abw-firstname,15080,fco-city,1986/11/01,tkuc-street,69,FM,FEDERATED STATES OF MICRONESIA,12 +796,jplol-name,akr-firstname,19760,rvj-city,1970/12/23,sqxe-street,128,SD,SOUTH DAKOTA,19 +797,iegsb-name,ujo-firstname,18480,abx-city,1974/02/24,esda-street,16,RI,RHODE ISLAND,20 +798,ypqix-name,hbp-firstname,13240,kdv-city,1986/05/20,iyaq-street,10,OH,OHIO,12 +799,awluy-name,ysq-firstname,13680,kcv-city,1986/12/16,rtbk-street,144,ID,IDAHO,14 +800,vtyuq-name,cup-firstname,13200,gcq-city,1970/11/05,omwn-street,156,AR,ARKANSAS,20 +801,hmvzq-name,jgx-firstname,18660,hbj-city,1982/03/30,hxik-street,59,MO,MISSOURI,19 +802,vhatl-name,ywe-firstname,10040,ytj-city,1982/02/10,lblz-street,62,CT,CONNECTICUT,14 +803,pvozw-name,pln-firstname,13980,tdu-city,1986/06/18,ulqg-street,8,MT,MONTANA,19 +804,mwmvk-name,ttp-firstname,14020,dcw-city,1982/08/26,lqrt-street,104,CO,COLORADO,18 +805,pmlsn-name,uxe-firstname,17680,udv-city,1962/12/27,usnv-street,155,PR,PUERTO RICO,12 +806,uuvrf-name,jxu-firstname,16360,vpo-city,1978/08/04,tpez-street,25,MS,MISSISSIPPI,11 +807,pvcck-name,nnx-firstname,19780,kfm-city,1962/07/05,aybu-street,151,CA,CALIFORNIA,19 +808,kdybn-name,qcg-firstname,11080,wno-city,1978/05/06,omzx-street,56,GU,GUAM,19 +809,ylapy-name,mou-firstname,11400,yky-city,1990/03/12,rcbx-street,167,WI,WISCONSIN,17 +810,ygvfd-name,hqp-firstname,13240,sfw-city,1958/05/16,svut-street,16,RI,RHODE ISLAND,20 +811,wgbsf-name,wgj-firstname,12000,itm-city,1966/07/31,xlwg-street,122,NV,NEVADA,24 +812,guhzx-name,hde-firstname,19260,kqz-city,1962/03/01,escf-street,84,NY,NEW YORK,21 +813,quyjf-name,vta-firstname,14440,yiw-city,1978/01/29,nsss-street,83,ND,NORTH DAKOTA,14 +814,fsbhc-name,ink-firstname,19220,qjo-city,1974/08/30,pwtn-street,140,WA,WASHINGTON,18 +815,zhzwu-name,pkl-firstname,17680,slh-city,1966/06/13,cspv-street,106,NJ,NEW JERSEY,17 +816,sqegy-name,ufg-firstname,19220,hry-city,1954/03/25,anrd-street,173,PW,PALAU,16 +817,bkiln-name,sjs-firstname,15100,fao-city,1958/09/04,inav-street,170,AR,ARKANSAS,20 +818,zynej-name,zgh-firstname,15100,spc-city,1962/01/25,zewf-street,85,GU,GUAM,19 +819,xzasj-name,kib-firstname,18080,rzn-city,1970/10/07,tlsi-street,190,SD,SOUTH DAKOTA,19 +820,tmqlc-name,jes-firstname,11560,vhc-city,1974/11/27,xxkh-street,36,MO,MISSOURI,19 +821,gowim-name,rcr-firstname,11200,kja-city,1974/06/06,zhkg-street,41,MO,MISSOURI,19 +822,bulti-name,cse-firstname,13660,ukw-city,1970/12/14,glrc-street,59,NV,NEVADA,24 +823,evxmh-name,gfc-firstname,13560,cbo-city,1986/03/05,sfkz-street,72,ID,IDAHO,14 +824,dbdop-name,swu-firstname,14760,tlv-city,1982/10/06,vprj-street,87,AR,ARKANSAS,20 +825,mgyeq-name,wko-firstname,15320,lxz-city,1990/03/22,mfwa-street,11,DE,DELAWARE,18 +826,bfplt-name,ard-firstname,10360,sou-city,1990/02/13,jniu-street,124,NC,NORTH CAROLINA,16 +827,dvbot-name,auq-firstname,16200,qkn-city,1978/03/20,qfzt-street,9,IL,ILLINOIS,13 +828,tncjm-name,kvn-firstname,11640,mlf-city,1954/02/08,igos-street,166,ID,IDAHO,14 +829,svoxr-name,rbc-firstname,10400,bny-city,1958/07/13,socq-street,198,MH,MARSHALL ISLANDS,14 +830,jxzxn-name,wau-firstname,14360,orx-city,1958/07/08,fssh-street,77,NM,NEW MEXICO,10 +831,dglke-name,dyc-firstname,13100,lgz-city,1982/12/18,qixe-street,158,MH,MARSHALL ISLANDS,14 +832,pewvm-name,asq-firstname,10840,czn-city,1966/04/02,wnac-street,84,MD,MARYLAND,18 +833,tfgsc-name,vxn-firstname,10100,wup-city,1982/02/05,qlsb-street,193,AK,ALASKA,19 +834,hmoxn-name,vti-firstname,13660,gvq-city,1974/11/25,dahy-street,123,HI,HAWAII,22 +835,bexku-name,mvy-firstname,13080,wrx-city,1966/09/25,ovag-street,86,GA,GEORGIA,14 +836,ksffn-name,ubg-firstname,18600,gsu-city,1958/02/03,vepx-street,58,NE,NEBRASKA,20 +837,ngfsu-name,pgq-firstname,10400,pag-city,1986/06/19,rwqz-street,123,DE,DELAWARE,18 +838,mopuh-name,vdg-firstname,14160,xbe-city,1970/11/22,vbak-street,103,MA,MASSACHUSETTS,18 +839,rifdh-name,eky-firstname,12900,uwr-city,1978/04/02,yysd-street,81,GU,GUAM,19 +840,avlwf-name,xpn-firstname,14940,ebu-city,1974/08/06,vcki-street,154,PW,PALAU,16 +841,avokq-name,dzw-firstname,14680,xnx-city,1954/02/23,cmis-street,175,NH,NEW HAMPSHIRE,14 +842,nahim-name,yvv-firstname,18020,egx-city,1978/05/05,rpeq-street,86,NJ,NEW JERSEY,17 +843,kehud-name,gus-firstname,13200,bwc-city,1962/02/19,aagh-street,166,OK,OKLAHOMA,26 +844,tkfjt-name,kch-firstname,11160,vuw-city,1974/07/20,mkli-street,119,WY,WYOMING,16 +845,gcwwn-name,rpb-firstname,10640,yum-city,1982/05/17,vqgz-street,157,OR,OREGON,12 +846,piobc-name,pii-firstname,17360,vcs-city,1950/06/02,obsu-street,105,FM,FEDERATED STATES OF MICRONESIA,12 +847,nfguz-name,rtb-firstname,14380,zrh-city,1982/03/01,jltk-street,164,PW,PALAU,16 +848,kayxc-name,gew-firstname,15780,miz-city,1959/01/22,bckb-street,85,ME,MAINE,19 +849,kwjrg-name,lwy-firstname,19080,qym-city,1986/08/26,fagw-street,181,OR,OREGON,12 +850,joacl-name,nay-firstname,15120,wdm-city,1982/12/10,goug-street,6,VT,VERMONT,16 +851,fmnme-name,ctx-firstname,13940,qnj-city,1954/04/12,cacp-street,155,AL,ALABAMA,14 +852,cqjtz-name,vqp-firstname,16480,san-city,1966/10/16,oijg-street,94,NY,NEW YORK,21 +853,tdndt-name,qya-firstname,16320,log-city,1962/05/08,pvtk-street,151,MP,NORTHERN MARIANA ISLANDS,17 +854,cquwe-name,lzs-firstname,12940,vnf-city,1986/08/16,tsfg-street,181,SD,SOUTH DAKOTA,19 +855,dbfxb-name,glb-firstname,18420,ecz-city,1978/11/28,fpuk-street,42,PW,PALAU,16 +856,odwcc-name,tcz-firstname,17420,ffa-city,1954/06/12,kirb-street,158,WI,WISCONSIN,17 +857,qctkr-name,wpo-firstname,16980,upo-city,1978/06/21,gfrm-street,116,TX,TEXAS,16 +858,qikks-name,eep-firstname,18560,tof-city,1986/08/11,trqf-street,64,WA,WASHINGTON,18 +859,gbltk-name,jny-firstname,12080,tdu-city,1978/09/15,wsii-street,133,ID,IDAHO,14 +860,cirdu-name,dzw-firstname,11340,stm-city,1974/04/04,qnat-street,48,PA,PENNSYLVANIA,15 +861,xukav-name,rep-firstname,18240,pzu-city,1950/11/16,piru-street,24,PW,PALAU,16 +862,dqxqx-name,eca-firstname,16080,ddl-city,1982/08/15,snln-street,109,AK,ALASKA,19 +863,jxcxv-name,ipw-firstname,19200,ctv-city,1982/12/03,zazz-street,171,TN,TENNESSEE,11 +864,ddgxr-name,bwx-firstname,18860,giq-city,1954/05/11,ukyp-street,161,SC,SOUTH CAROLINA,8 +865,wcboj-name,cqb-firstname,15080,srk-city,1978/04/28,pycv-street,12,PW,PALAU,16 +866,ifgzt-name,bde-firstname,17700,whw-city,1978/05/05,qfgv-street,162,SD,SOUTH DAKOTA,19 +867,riwqf-name,qil-firstname,18480,mce-city,1990/09/07,qxyc-street,69,MP,NORTHERN MARIANA ISLANDS,17 +868,zdudw-name,yxg-firstname,17380,apv-city,1954/09/09,wymd-street,195,KS,KANSAS,18 +869,szvfk-name,nei-firstname,13660,xqq-city,1974/10/14,ygbr-street,129,KY,KENTUCKY,18 +870,lrgna-name,lqv-firstname,16840,woh-city,1978/09/16,ibzb-street,87,IL,ILLINOIS,13 +871,jeaqx-name,kdj-firstname,16840,lpn-city,1979/01/26,guxb-street,10,WA,WASHINGTON,18 +872,ubvth-name,njr-firstname,14920,hbz-city,1966/10/20,knnm-street,69,AL,ALABAMA,14 +873,opkuq-name,sbc-firstname,10520,wpu-city,1974/10/24,tzoa-street,10,OK,OKLAHOMA,26 +874,eeesy-name,sei-firstname,13420,xhc-city,1978/01/10,wsmi-street,48,AK,ALASKA,19 +875,njzrw-name,yep-firstname,13680,hdj-city,1974/05/12,bhba-street,42,ME,MAINE,19 +876,caria-name,jaf-firstname,18660,scx-city,1962/12/18,envv-street,158,GU,GUAM,19 +877,sdwui-name,dax-firstname,17780,mdr-city,1958/12/22,orah-street,167,AK,ALASKA,19 +878,pnklv-name,not-firstname,11160,gfk-city,1982/08/07,muqk-street,6,AZ,ARIZONA,15 +879,xhqwk-name,cgz-firstname,19940,nji-city,1962/05/06,ihqj-street,97,TN,TENNESSEE,11 +880,oigoy-name,ccp-firstname,14060,ase-city,1958/05/20,faef-street,133,MD,MARYLAND,18 +881,nysrz-name,nil-firstname,17700,ile-city,1966/09/13,aqiy-street,25,WI,WISCONSIN,17 +882,wrkvm-name,ftf-firstname,17780,wim-city,1982/12/12,kzed-street,57,MN,MINNESOTA,23 +883,zngvz-name,tcp-firstname,11640,vba-city,1982/05/29,phle-street,105,OK,OKLAHOMA,26 +884,wdubl-name,his-firstname,19840,ybx-city,1978/08/06,upqg-street,80,TN,TENNESSEE,11 +885,fxvpc-name,yxz-firstname,13380,ocv-city,1958/04/08,shtt-street,28,IL,ILLINOIS,13 +886,blbvw-name,nln-firstname,17740,gwm-city,1955/01/28,ratd-street,98,UT,UTAH,19 +887,ajxnx-name,utp-firstname,15540,eak-city,1950/07/01,zlzg-street,74,OR,OREGON,12 +888,aeeid-name,bmc-firstname,19100,qtd-city,1958/10/28,sgwc-street,127,OH,OHIO,12 +889,ypwhc-name,ojx-firstname,11480,zib-city,1967/01/09,gpfp-street,76,UT,UTAH,19 +890,kuxdp-name,exg-firstname,14840,cvh-city,1986/01/22,jbif-street,108,TN,TENNESSEE,11 +891,ujwmw-name,aqz-firstname,14080,mwz-city,1978/07/22,eggm-street,8,MN,MINNESOTA,23 +892,xogup-name,pps-firstname,11740,bic-city,1970/11/26,thtu-street,34,ME,MAINE,19 +893,vmrty-name,rmg-firstname,11060,cna-city,1970/08/27,uknc-street,24,GA,GEORGIA,14 +894,rrwco-name,dji-firstname,18380,oln-city,1974/01/02,fzqy-street,129,MI,MICHIGAN,11 +895,dvcbl-name,nam-firstname,15040,sbp-city,1962/03/02,txma-street,170,HI,HAWAII,22 +896,tbsrx-name,krm-firstname,15220,gao-city,1959/01/03,fmhf-street,125,TX,TEXAS,16 +897,ymjve-name,nmc-firstname,19280,yux-city,1974/12/11,ycqn-street,65,FL,FLORIDA,20 +898,rycyz-name,prd-firstname,15920,oml-city,1982/09/13,ivie-street,30,OK,OKLAHOMA,26 +899,agxhw-name,zla-firstname,14780,pkw-city,1970/04/28,mcrt-street,139,MN,MINNESOTA,23 +900,bbdii-name,wkd-firstname,15220,eup-city,1962/05/01,neqn-street,191,ID,IDAHO,14 +901,svszz-name,gyt-firstname,12900,sav-city,1954/09/30,gqnm-street,117,SC,SOUTH CAROLINA,8 +902,dqidv-name,ooi-firstname,17340,krq-city,1978/06/24,hjtm-street,106,NJ,NEW JERSEY,17 +903,blcfn-name,lrz-firstname,19240,sav-city,1978/03/01,xrfr-street,8,RI,RHODE ISLAND,20 +904,acgjp-name,ncp-firstname,17900,snk-city,1974/10/27,qbrs-street,65,IA,IOWA,14 +905,eloeo-name,tmh-firstname,17500,use-city,1971/01/05,hkrt-street,23,PA,PENNSYLVANIA,15 +906,lxogw-name,hlp-firstname,10960,yhi-city,1990/10/25,ewnk-street,7,GU,GUAM,19 +907,gvwtl-name,dmy-firstname,19920,fqc-city,1970/05/02,mpze-street,63,LA,LOUISIANA,21 +908,vrrxx-name,xmg-firstname,17160,yks-city,1978/08/14,hikr-street,165,PW,PALAU,16 +909,hkpuu-name,ofx-firstname,17040,hid-city,1978/02/03,rkrn-street,8,UT,UTAH,19 +910,bupib-name,ftw-firstname,16620,fsw-city,1971/01/09,uzxm-street,95,RI,RHODE ISLAND,20 +911,siwci-name,fir-firstname,11180,sqp-city,1974/06/27,pgeq-street,134,AS,AMERICAN SAMOA,15 +912,mpkrp-name,etl-firstname,10560,kty-city,1958/06/02,cpug-street,67,CA,CALIFORNIA,19 +913,itfnp-name,ncv-firstname,16440,dgw-city,1958/07/29,xdkl-street,131,OK,OKLAHOMA,26 +914,oyvqm-name,lwu-firstname,14760,dzo-city,1958/07/09,ubqh-street,57,IA,IOWA,14 +915,kuila-name,umo-firstname,11880,rhy-city,1982/09/17,jwha-street,81,AK,ALASKA,19 +916,zduzo-name,gmh-firstname,12360,oxv-city,1990/06/26,dbxf-street,76,VI,VIRGIN ISLANDS,14 +917,cxggf-name,fld-firstname,12900,sif-city,1954/09/08,wdis-street,142,WI,WISCONSIN,17 +918,euelo-name,sbi-firstname,19340,shq-city,1986/04/20,qmja-street,3,GA,GEORGIA,14 +919,papgo-name,oyu-firstname,10280,bet-city,1970/06/19,otzt-street,155,CA,CALIFORNIA,19 +920,dtjeu-name,gvg-firstname,11980,aie-city,1954/10/17,tcpt-street,77,NY,NEW YORK,21 +921,xryzl-name,oqb-firstname,16540,era-city,1962/08/27,aywk-street,118,VI,VIRGIN ISLANDS,14 +922,pydqj-name,rwh-firstname,15640,ekn-city,1978/09/16,yecx-street,30,WA,WASHINGTON,18 +923,picbf-name,cru-firstname,16040,iaa-city,1986/07/01,ttsg-street,112,MO,MISSOURI,19 +924,jfqgk-name,ghq-firstname,10420,yap-city,1958/06/19,qzbd-street,1,NV,NEVADA,24 +925,iqoex-name,dha-firstname,17340,fqq-city,1966/05/09,mbkz-street,151,VT,VERMONT,16 +926,xlugu-name,fae-firstname,12060,uvm-city,1954/11/21,dhin-street,100,CT,CONNECTICUT,14 +927,pxbfc-name,cia-firstname,15160,xrr-city,1986/06/20,psch-street,122,MT,MONTANA,19 +928,koqls-name,teq-firstname,16100,vtl-city,1982/04/28,qfxy-street,11,ME,MAINE,19 +929,nrhco-name,cwj-firstname,17380,zzw-city,1958/06/19,dcir-street,188,WA,WASHINGTON,18 +930,rrxbl-name,gfc-firstname,13160,ruo-city,1982/06/02,yers-street,108,MH,MARSHALL ISLANDS,14 +931,rqtri-name,oqi-firstname,19780,jvk-city,1970/12/08,dfnt-street,196,IA,IOWA,14 +932,txjky-name,gvy-firstname,19040,jwu-city,1982/03/01,pacb-street,83,UT,UTAH,19 +933,efrbq-name,tgu-firstname,13200,xgk-city,1970/04/10,ipjk-street,156,WI,WISCONSIN,17 +934,xzrxi-name,lwt-firstname,16980,zll-city,1982/01/27,geic-street,42,LA,LOUISIANA,21 +935,uafzk-name,wox-firstname,10240,agj-city,1978/07/03,wdwx-street,13,TX,TEXAS,16 +936,npomk-name,upa-firstname,19060,oad-city,1986/06/27,qkua-street,200,MP,NORTHERN MARIANA ISLANDS,17 +937,bepdw-name,box-firstname,13280,sxr-city,1978/04/15,rnub-street,19,ME,MAINE,19 +938,qbbcf-name,igs-firstname,15440,xrs-city,1958/09/13,yhum-street,176,MA,MASSACHUSETTS,18 +939,hirau-name,vyi-firstname,15980,iyv-city,1962/09/17,gqqy-street,48,FM,FEDERATED STATES OF MICRONESIA,12 +940,fzpiq-name,rco-firstname,10660,wpc-city,1978/03/30,kktg-street,149,NY,NEW YORK,21 +941,kvtgo-name,wkl-firstname,13840,yik-city,1962/02/18,goyb-street,15,NV,NEVADA,24 +942,mpljt-name,xyx-firstname,14880,kdx-city,1982/05/25,fkgb-street,50,WV,WEST VIRGINIA,22 +943,cxjtz-name,zkx-firstname,18500,mrg-city,1974/09/04,seup-street,139,MP,NORTHERN MARIANA ISLANDS,17 +944,cbkvb-name,uji-firstname,13260,rtg-city,1954/02/08,oedv-street,5,AZ,ARIZONA,15 +945,qsujg-name,hig-firstname,11980,obk-city,1974/12/24,awap-street,140,WI,WISCONSIN,17 +946,faqty-name,wml-firstname,19180,qmg-city,1958/07/06,zxlq-street,9,NV,NEVADA,24 +947,cdwge-name,aeu-firstname,16940,axh-city,1970/12/16,nkhw-street,165,RI,RHODE ISLAND,20 +948,ctmij-name,tqq-firstname,12600,apv-city,1990/12/10,egyw-street,94,MD,MARYLAND,18 +949,axcvk-name,dhn-firstname,11900,osf-city,1982/03/17,tzwx-street,71,OH,OHIO,12 +950,flhxd-name,tty-firstname,10380,nfn-city,1970/04/16,iweq-street,144,MN,MINNESOTA,23 +951,bnnjj-name,ttq-firstname,11360,shi-city,1966/11/16,xhde-street,125,MI,MICHIGAN,11 +952,ohowr-name,rlf-firstname,16380,dzs-city,1982/05/12,yiwv-street,46,IL,ILLINOIS,13 +953,ofqhs-name,pyy-firstname,12100,pfy-city,1958/04/28,xqtd-street,148,UT,UTAH,19 +954,quyhm-name,oln-firstname,12140,lqe-city,1978/09/28,prlu-street,151,MO,MISSOURI,19 +955,ailpr-name,cut-firstname,13200,yqu-city,1950/07/05,fxsk-street,101,IA,IOWA,14 +956,zeyok-name,vhb-firstname,17240,nih-city,1986/11/30,bvms-street,86,WA,WASHINGTON,18 +957,xrfau-name,owh-firstname,17180,tep-city,1954/03/02,qcmo-street,45,NJ,NEW JERSEY,17 +958,nuoiw-name,rxs-firstname,11320,uef-city,1962/04/23,oiuf-street,117,MA,MASSACHUSETTS,18 +959,imjdp-name,mlh-firstname,19020,mpi-city,1962/06/28,obxd-street,11,PA,PENNSYLVANIA,15 +960,ynmgr-name,uzp-firstname,15500,oxs-city,1986/07/15,epkx-street,92,GA,GEORGIA,14 +961,wcucp-name,qlt-firstname,12460,sow-city,1986/05/17,qoxd-street,40,WV,WEST VIRGINIA,22 +962,ugiex-name,ebp-firstname,15640,vgo-city,1974/09/28,ggdc-street,22,MH,MARSHALL ISLANDS,14 +963,nsilq-name,vxf-firstname,19280,zzm-city,1986/03/13,kxrw-street,68,VA,VIRGINIA,20 +964,whtok-name,oqs-firstname,18340,ddf-city,1954/10/13,toaz-street,100,VT,VERMONT,16 +965,lbwzb-name,rgg-firstname,15340,bla-city,1978/12/10,usrd-street,175,TN,TENNESSEE,11 +966,lcbat-name,nnd-firstname,16200,ebl-city,1990/11/20,emud-street,144,MI,MICHIGAN,11 +967,alouv-name,omr-firstname,14980,ouw-city,1958/06/11,xbnb-street,90,AZ,ARIZONA,15 +968,nvqwv-name,blm-firstname,17880,nip-city,1974/12/14,sgzi-street,33,AS,AMERICAN SAMOA,15 +969,wdidq-name,qwd-firstname,15480,eis-city,1979/01/22,qqbv-street,43,NH,NEW HAMPSHIRE,14 +970,jmlfw-name,gal-firstname,12680,fjt-city,1955/01/10,iqnm-street,81,GU,GUAM,19 +971,feoxv-name,mme-firstname,18020,oed-city,1986/01/13,jtud-street,73,NV,NEVADA,24 +972,gsdoz-name,oxc-firstname,10420,jff-city,1978/02/20,jnom-street,62,OR,OREGON,12 +973,hklot-name,rur-firstname,13900,nur-city,1986/06/05,nkmx-street,195,SD,SOUTH DAKOTA,19 +974,smemp-name,ehc-firstname,10080,wrv-city,1950/04/10,wfrv-street,93,FL,FLORIDA,20 +975,htgft-name,nzy-firstname,16960,gwe-city,1966/05/09,abiz-street,58,UT,UTAH,19 +976,jaikt-name,tls-firstname,17280,whv-city,1966/09/19,wfzd-street,70,ME,MAINE,19 +977,yshns-name,zhh-firstname,13200,dty-city,1978/12/13,hiht-street,158,KS,KANSAS,18 +978,selvf-name,zcr-firstname,12900,phm-city,1958/12/28,pont-street,149,GU,GUAM,19 +979,qoxyo-name,oth-firstname,10280,uic-city,1986/03/16,havi-street,153,ID,IDAHO,14 +980,uabxl-name,cid-firstname,18000,cea-city,1982/02/18,jykd-street,127,WV,WEST VIRGINIA,22 +981,hjnpb-name,wwh-firstname,12400,fwd-city,1978/10/01,ryqy-street,101,OH,OHIO,12 +982,zcqum-name,ecp-firstname,15820,rpu-city,1958/11/11,fpoi-street,118,undefined,undefined,13 +983,cetkk-name,btl-firstname,18660,xrq-city,1970/04/26,mkuo-street,167,TX,TEXAS,16 +984,laofd-name,fqf-firstname,18180,ddy-city,1986/04/01,qjar-street,14,OR,OREGON,12 +985,xjtie-name,tpq-firstname,16800,fgb-city,1970/03/09,nqpk-street,108,MA,MASSACHUSETTS,18 +986,txrwc-name,ygc-firstname,17880,ipb-city,1962/04/26,zomj-street,189,DC,DISTRICT OF COLUMBIA,16 +987,csicd-name,yzm-firstname,18240,nwu-city,1978/05/03,ishz-street,24,LA,LOUISIANA,21 +988,njtnl-name,nxa-firstname,10360,ols-city,1990/12/01,joem-street,101,DE,DELAWARE,18 +989,dclnv-name,hve-firstname,17080,amp-city,1978/05/22,jfuv-street,17,WI,WISCONSIN,17 +990,jrazh-name,yec-firstname,14000,tsi-city,1986/09/02,guzz-street,119,KS,KANSAS,18 +991,hpxko-name,psn-firstname,11900,ocf-city,1970/04/21,gvbf-street,12,OK,OKLAHOMA,26 +992,lggcw-name,sey-firstname,17260,ike-city,1958/09/19,yrnf-street,34,NE,NEBRASKA,20 +993,cxifr-name,agu-firstname,14360,ujj-city,1954/09/28,widd-street,127,undefined,undefined,13 +994,kfwgh-name,qaa-firstname,17900,kis-city,1970/10/02,wwgu-street,134,MD,MARYLAND,18 +995,pcizt-name,fdt-firstname,15320,tzn-city,1982/09/08,nqii-street,163,TN,TENNESSEE,11 +996,hyskd-name,ott-firstname,19800,zja-city,1962/03/05,tjpp-street,108,OH,OHIO,12 +997,nfdtl-name,jxu-firstname,12980,isx-city,1982/07/05,dldc-street,166,CA,CALIFORNIA,19 +998,xtukc-name,aih-firstname,15360,ctn-city,1974/11/10,qhwy-street,102,MI,MICHIGAN,11 +999,qfwcj-name,fdu-firstname,10100,enp-city,1950/01/11,qqga-street,19,TX,TEXAS,16 +1000,uvhph-name,vri-firstname,11920,tjs-city,1974/04/28,czih-street,151,NE,NEBRASKA,20 diff --git a/integration-tests/spark-native/datasets/0005-filter-false-golden.csv b/integration-tests/spark-native/datasets/0005-filter-false-golden.csv new file mode 100644 index 00000000000..761a190eeda --- /dev/null +++ b/integration-tests/spark-native/datasets/0005-filter-false-golden.csv @@ -0,0 +1,954 @@ +id,stateCode,state +1,AK,ALASKA +2,GA,GEORGIA +3,NJ,NEW JERSEY +4,NY,NEW YORK +5,IN,INDIANA +6,MD,MARYLAND +7,PA,PENNSYLVANIA +8,PW,PALAU +10,AR,ARKANSAS +11,NH,NEW HAMPSHIRE +12,CA,CALIFORNIA +13,OK,OKLAHOMA +14,MT,MONTANA +15,AS,AMERICAN SAMOA +16,MA,MASSACHUSETTS +17,KY,KENTUCKY +18,WI,WISCONSIN +19,ME,MAINE +20,MD,MARYLAND +21,TN,TENNESSEE +22,VI,VIRGIN ISLANDS +24,WA,WASHINGTON +25,CA,CALIFORNIA +26,CA,CALIFORNIA +27,MO,MISSOURI +28,NV,NEVADA +29,MS,MISSISSIPPI +30,GA,GEORGIA +31,OK,OKLAHOMA +32,SD,SOUTH DAKOTA +33,IN,INDIANA +34,NH,NEW HAMPSHIRE +35,MS,MISSISSIPPI +36,NV,NEVADA +37,PW,PALAU +38,undefined,undefined +39,OK,OKLAHOMA +40,DC,DISTRICT OF COLUMBIA +41,CO,COLORADO +42,AK,ALASKA +43,VI,VIRGIN ISLANDS +44,CA,CALIFORNIA +45,TX,TEXAS +46,PW,PALAU +47,UT,UTAH +48,WV,WEST VIRGINIA +50,LA,LOUISIANA +51,GA,GEORGIA +54,MO,MISSOURI +55,KS,KANSAS +56,HI,HAWAII +57,GU,GUAM +58,DC,DISTRICT OF COLUMBIA +59,FL,FLORIDA +60,RI,RHODE ISLAND +61,SD,SOUTH DAKOTA +62,NV,NEVADA +63,AS,AMERICAN SAMOA +64,OK,OKLAHOMA +65,FM,FEDERATED STATES OF MICRONESIA +66,NY,NEW YORK +67,VA,VIRGINIA +68,PW,PALAU +69,WA,WASHINGTON +70,FL,FLORIDA +71,ID,IDAHO +72,MN,MINNESOTA +75,VA,VIRGINIA +76,CO,COLORADO +77,VT,VERMONT +78,MD,MARYLAND +79,NM,NEW MEXICO +80,LA,LOUISIANA +81,GA,GEORGIA +82,GU,GUAM +83,TX,TEXAS +84,IN,INDIANA +85,NV,NEVADA +86,CT,CONNECTICUT +87,ME,MAINE +88,SD,SOUTH DAKOTA +89,DC,DISTRICT OF COLUMBIA +90,TX,TEXAS +91,ME,MAINE +92,WV,WEST VIRGINIA +93,NV,NEVADA +94,MO,MISSOURI +95,MA,MASSACHUSETTS +96,FL,FLORIDA +97,KS,KANSAS +98,RI,RHODE ISLAND +99,MD,MARYLAND +100,NM,NEW MEXICO +101,WI,WISCONSIN +103,MI,MICHIGAN +104,OK,OKLAHOMA +106,MO,MISSOURI +107,AZ,ARIZONA +108,MH,MARSHALL ISLANDS +109,GA,GEORGIA +110,HI,HAWAII +111,AK,ALASKA +112,AL,ALABAMA +113,GA,GEORGIA +114,MH,MARSHALL ISLANDS +115,KS,KANSAS +116,WV,WEST VIRGINIA +117,OK,OKLAHOMA +118,NV,NEVADA +120,WA,WASHINGTON +121,ID,IDAHO +122,NY,NEW YORK +123,VT,VERMONT +124,AK,ALASKA +125,FL,FLORIDA +126,WI,WISCONSIN +127,MO,MISSOURI +128,AS,AMERICAN SAMOA +129,IN,INDIANA +130,UT,UTAH +131,OR,OREGON +132,NE,NEBRASKA +133,WV,WEST VIRGINIA +134,TX,TEXAS +135,CA,CALIFORNIA +137,MT,MONTANA +138,WY,WYOMING +139,SD,SOUTH DAKOTA +140,LA,LOUISIANA +141,NJ,NEW JERSEY +142,NE,NEBRASKA +143,HI,HAWAII +144,MT,MONTANA +145,NH,NEW HAMPSHIRE +146,DE,DELAWARE +147,MN,MINNESOTA +148,NH,NEW HAMPSHIRE +150,DC,DISTRICT OF COLUMBIA +151,IN,INDIANA +152,AR,ARKANSAS +153,MA,MASSACHUSETTS +154,NY,NEW YORK +155,MT,MONTANA +156,UT,UTAH +157,NY,NEW YORK +158,WY,WYOMING +160,NJ,NEW JERSEY +161,NJ,NEW JERSEY +162,NJ,NEW JERSEY +163,LA,LOUISIANA +164,MA,MASSACHUSETTS +165,CO,COLORADO +166,KY,KENTUCKY +167,CO,COLORADO +168,KS,KANSAS +169,OR,OREGON +170,MN,MINNESOTA +171,UT,UTAH +172,NE,NEBRASKA +173,MS,MISSISSIPPI +174,WY,WYOMING +175,KY,KENTUCKY +176,SC,SOUTH CAROLINA +177,CA,CALIFORNIA +178,FL,FLORIDA +179,PR,PUERTO RICO +180,AZ,ARIZONA +181,CA,CALIFORNIA +182,NE,NEBRASKA +183,PR,PUERTO RICO +185,MS,MISSISSIPPI +186,MO,MISSOURI +187,AR,ARKANSAS +188,DE,DELAWARE +189,OK,OKLAHOMA +190,IN,INDIANA +191,UT,UTAH +192,WA,WASHINGTON +193,AS,AMERICAN SAMOA +194,KY,KENTUCKY +196,MH,MARSHALL ISLANDS +197,VA,VIRGINIA +198,MT,MONTANA +199,CO,COLORADO +201,MS,MISSISSIPPI +202,FL,FLORIDA +203,PA,PENNSYLVANIA +204,VA,VIRGINIA +205,NM,NEW MEXICO +206,NV,NEVADA +207,UT,UTAH +208,GU,GUAM +209,PA,PENNSYLVANIA +210,KS,KANSAS +211,VI,VIRGIN ISLANDS +212,FL,FLORIDA +213,TX,TEXAS +214,MA,MASSACHUSETTS +215,NJ,NEW JERSEY +216,KS,KANSAS +217,NV,NEVADA +218,AK,ALASKA +219,RI,RHODE ISLAND +220,NY,NEW YORK +221,NY,NEW YORK +222,HI,HAWAII +223,SD,SOUTH DAKOTA +224,VT,VERMONT +225,LA,LOUISIANA +226,VT,VERMONT +227,FM,FEDERATED STATES OF MICRONESIA +228,MT,MONTANA +230,LA,LOUISIANA +231,KY,KENTUCKY +232,NM,NEW MEXICO +233,WA,WASHINGTON +234,MI,MICHIGAN +235,WI,WISCONSIN +236,DC,DISTRICT OF COLUMBIA +237,NM,NEW MEXICO +238,MN,MINNESOTA +239,PA,PENNSYLVANIA +240,PR,PUERTO RICO +242,WV,WEST VIRGINIA +243,MI,MICHIGAN +244,undefined,undefined +245,AR,ARKANSAS +246,ME,MAINE +247,AL,ALABAMA +248,IN,INDIANA +249,MD,MARYLAND +250,IL,ILLINOIS +251,DC,DISTRICT OF COLUMBIA +252,TX,TEXAS +253,IN,INDIANA +254,AR,ARKANSAS +255,MH,MARSHALL ISLANDS +256,DC,DISTRICT OF COLUMBIA +257,VT,VERMONT +258,CO,COLORADO +259,GU,GUAM +260,MD,MARYLAND +261,AZ,ARIZONA +262,MT,MONTANA +263,AK,ALASKA +265,OK,OKLAHOMA +266,MO,MISSOURI +267,MT,MONTANA +268,ME,MAINE +269,FL,FLORIDA +270,KS,KANSAS +271,GA,GEORGIA +272,CA,CALIFORNIA +273,PR,PUERTO RICO +274,OK,OKLAHOMA +275,MN,MINNESOTA +276,MN,MINNESOTA +277,ME,MAINE +278,VI,VIRGIN ISLANDS +279,HI,HAWAII +280,CO,COLORADO +281,CO,COLORADO +282,WV,WEST VIRGINIA +283,MH,MARSHALL ISLANDS +284,UT,UTAH +285,ME,MAINE +286,WA,WASHINGTON +287,PA,PENNSYLVANIA +288,MN,MINNESOTA +289,VA,VIRGINIA +290,WV,WEST VIRGINIA +292,CO,COLORADO +293,VT,VERMONT +294,LA,LOUISIANA +295,NH,NEW HAMPSHIRE +296,FL,FLORIDA +297,WV,WEST VIRGINIA +298,VI,VIRGIN ISLANDS +300,WA,WASHINGTON +301,WV,WEST VIRGINIA +302,HI,HAWAII +303,NM,NEW MEXICO +304,CA,CALIFORNIA +305,VT,VERMONT +306,TX,TEXAS +307,PA,PENNSYLVANIA +308,CA,CALIFORNIA +309,UT,UTAH +310,PR,PUERTO RICO +311,WY,WYOMING +312,NE,NEBRASKA +313,undefined,undefined +314,IL,ILLINOIS +315,VI,VIRGIN ISLANDS +316,NH,NEW HAMPSHIRE +317,DC,DISTRICT OF COLUMBIA +318,GU,GUAM +320,FL,FLORIDA +321,AR,ARKANSAS +322,WY,WYOMING +323,AZ,ARIZONA +325,AL,ALABAMA +326,VT,VERMONT +327,KY,KENTUCKY +328,VA,VIRGINIA +329,CT,CONNECTICUT +330,PW,PALAU +331,OR,OREGON +332,GU,GUAM +333,SD,SOUTH DAKOTA +335,AR,ARKANSAS +336,SD,SOUTH DAKOTA +337,WA,WASHINGTON +338,SD,SOUTH DAKOTA +339,AZ,ARIZONA +340,WI,WISCONSIN +341,SC,SOUTH CAROLINA +342,IA,IOWA +343,PW,PALAU +344,AL,ALABAMA +345,NE,NEBRASKA +346,FL,FLORIDA +347,SD,SOUTH DAKOTA +348,undefined,undefined +349,undefined,undefined +350,VI,VIRGIN ISLANDS +351,AR,ARKANSAS +352,KY,KENTUCKY +353,IN,INDIANA +354,MT,MONTANA +355,HI,HAWAII +356,NE,NEBRASKA +357,UT,UTAH +358,PR,PUERTO RICO +359,MS,MISSISSIPPI +360,MA,MASSACHUSETTS +361,CT,CONNECTICUT +362,CT,CONNECTICUT +363,MI,MICHIGAN +364,MT,MONTANA +365,AS,AMERICAN SAMOA +366,MI,MICHIGAN +367,ID,IDAHO +368,IL,ILLINOIS +369,DC,DISTRICT OF COLUMBIA +370,RI,RHODE ISLAND +371,KS,KANSAS +372,VA,VIRGINIA +373,NE,NEBRASKA +374,LA,LOUISIANA +375,MS,MISSISSIPPI +376,HI,HAWAII +377,WY,WYOMING +378,HI,HAWAII +379,MN,MINNESOTA +380,AS,AMERICAN SAMOA +382,WY,WYOMING +383,VI,VIRGIN ISLANDS +384,TX,TEXAS +385,VA,VIRGINIA +386,AL,ALABAMA +387,WY,WYOMING +388,undefined,undefined +389,FL,FLORIDA +390,NE,NEBRASKA +391,ME,MAINE +392,DC,DISTRICT OF COLUMBIA +394,RI,RHODE ISLAND +395,NH,NEW HAMPSHIRE +396,MN,MINNESOTA +397,UT,UTAH +398,CT,CONNECTICUT +399,AZ,ARIZONA +400,AR,ARKANSAS +401,SC,SOUTH CAROLINA +402,NV,NEVADA +403,NV,NEVADA +404,WY,WYOMING +405,CT,CONNECTICUT +406,MO,MISSOURI +407,MN,MINNESOTA +408,VT,VERMONT +409,WV,WEST VIRGINIA +410,AZ,ARIZONA +411,MN,MINNESOTA +412,AS,AMERICAN SAMOA +413,LA,LOUISIANA +414,VA,VIRGINIA +415,NE,NEBRASKA +416,ME,MAINE +417,DE,DELAWARE +418,SC,SOUTH CAROLINA +419,OK,OKLAHOMA +420,KS,KANSAS +421,GA,GEORGIA +422,CA,CALIFORNIA +423,MO,MISSOURI +424,NJ,NEW JERSEY +425,SC,SOUTH CAROLINA +426,GU,GUAM +427,OK,OKLAHOMA +428,MH,MARSHALL ISLANDS +429,NV,NEVADA +430,PA,PENNSYLVANIA +431,NV,NEVADA +432,HI,HAWAII +433,AR,ARKANSAS +434,KS,KANSAS +435,AR,ARKANSAS +436,OK,OKLAHOMA +437,LA,LOUISIANA +438,SD,SOUTH DAKOTA +439,WV,WEST VIRGINIA +440,PR,PUERTO RICO +441,OK,OKLAHOMA +442,WV,WEST VIRGINIA +443,NM,NEW MEXICO +445,VA,VIRGINIA +446,MN,MINNESOTA +447,AS,AMERICAN SAMOA +448,NV,NEVADA +449,CO,COLORADO +450,MA,MASSACHUSETTS +452,DE,DELAWARE +453,ME,MAINE +454,WA,WASHINGTON +455,NY,NEW YORK +456,MN,MINNESOTA +457,SD,SOUTH DAKOTA +458,PW,PALAU +459,MD,MARYLAND +460,MT,MONTANA +461,NJ,NEW JERSEY +462,FM,FEDERATED STATES OF MICRONESIA +463,KY,KENTUCKY +464,MD,MARYLAND +465,NM,NEW MEXICO +467,MD,MARYLAND +468,AK,ALASKA +469,VI,VIRGIN ISLANDS +470,LA,LOUISIANA +471,NY,NEW YORK +473,WY,WYOMING +474,FM,FEDERATED STATES OF MICRONESIA +475,OR,OREGON +476,WY,WYOMING +477,MT,MONTANA +478,UT,UTAH +479,FL,FLORIDA +480,MT,MONTANA +481,DE,DELAWARE +482,AK,ALASKA +483,MN,MINNESOTA +484,MN,MINNESOTA +485,RI,RHODE ISLAND +486,LA,LOUISIANA +487,WV,WEST VIRGINIA +488,undefined,undefined +489,DE,DELAWARE +490,IN,INDIANA +491,HI,HAWAII +492,NV,NEVADA +493,MT,MONTANA +494,WY,WYOMING +495,OH,OHIO +496,GU,GUAM +497,HI,HAWAII +498,IL,ILLINOIS +499,MO,MISSOURI +500,IA,IOWA +501,DE,DELAWARE +502,MT,MONTANA +503,AZ,ARIZONA +504,AR,ARKANSAS +505,AS,AMERICAN SAMOA +506,OH,OHIO +507,WY,WYOMING +508,NH,NEW HAMPSHIRE +509,RI,RHODE ISLAND +510,MD,MARYLAND +511,AR,ARKANSAS +512,OR,OREGON +513,MN,MINNESOTA +514,MA,MASSACHUSETTS +515,WV,WEST VIRGINIA +516,OK,OKLAHOMA +517,IA,IOWA +518,NE,NEBRASKA +519,AL,ALABAMA +520,MS,MISSISSIPPI +521,KY,KENTUCKY +522,ID,IDAHO +523,WV,WEST VIRGINIA +524,LA,LOUISIANA +526,NV,NEVADA +527,IN,INDIANA +528,MN,MINNESOTA +530,SD,SOUTH DAKOTA +531,ID,IDAHO +532,MN,MINNESOTA +533,FM,FEDERATED STATES OF MICRONESIA +534,FM,FEDERATED STATES OF MICRONESIA +535,IN,INDIANA +536,AK,ALASKA +538,NJ,NEW JERSEY +539,KY,KENTUCKY +540,AR,ARKANSAS +541,WI,WISCONSIN +542,NY,NEW YORK +543,CO,COLORADO +544,AL,ALABAMA +545,KS,KANSAS +546,KS,KANSAS +547,CO,COLORADO +548,VA,VIRGINIA +549,PA,PENNSYLVANIA +550,RI,RHODE ISLAND +551,SD,SOUTH DAKOTA +552,CO,COLORADO +553,IA,IOWA +554,NV,NEVADA +555,AK,ALASKA +556,DC,DISTRICT OF COLUMBIA +557,NH,NEW HAMPSHIRE +558,NE,NEBRASKA +559,IL,ILLINOIS +560,VA,VIRGINIA +561,CA,CALIFORNIA +562,VA,VIRGINIA +563,CO,COLORADO +564,DE,DELAWARE +565,AL,ALABAMA +566,LA,LOUISIANA +567,SC,SOUTH CAROLINA +568,OH,OHIO +569,GU,GUAM +570,FM,FEDERATED STATES OF MICRONESIA +571,AL,ALABAMA +572,NY,NEW YORK +573,MS,MISSISSIPPI +574,NJ,NEW JERSEY +576,AK,ALASKA +577,NH,NEW HAMPSHIRE +578,KS,KANSAS +579,NE,NEBRASKA +581,KS,KANSAS +582,TN,TENNESSEE +583,AK,ALASKA +584,WA,WASHINGTON +585,IA,IOWA +586,VT,VERMONT +588,CO,COLORADO +589,undefined,undefined +590,WV,WEST VIRGINIA +591,VT,VERMONT +592,IN,INDIANA +593,HI,HAWAII +594,IL,ILLINOIS +595,MA,MASSACHUSETTS +596,CT,CONNECTICUT +597,LA,LOUISIANA +598,OH,OHIO +599,NY,NEW YORK +600,CA,CALIFORNIA +601,NE,NEBRASKA +602,IA,IOWA +603,PA,PENNSYLVANIA +605,MA,MASSACHUSETTS +606,OR,OREGON +607,RI,RHODE ISLAND +608,AL,ALABAMA +610,RI,RHODE ISLAND +611,LA,LOUISIANA +612,TX,TEXAS +613,CT,CONNECTICUT +614,MS,MISSISSIPPI +615,KY,KENTUCKY +616,AS,AMERICAN SAMOA +617,NM,NEW MEXICO +618,NJ,NEW JERSEY +619,HI,HAWAII +620,OK,OKLAHOMA +621,MA,MASSACHUSETTS +622,OK,OKLAHOMA +623,AS,AMERICAN SAMOA +624,CO,COLORADO +625,LA,LOUISIANA +626,CT,CONNECTICUT +627,NY,NEW YORK +628,NV,NEVADA +629,ID,IDAHO +630,FL,FLORIDA +631,MH,MARSHALL ISLANDS +632,DC,DISTRICT OF COLUMBIA +633,IA,IOWA +634,VI,VIRGIN ISLANDS +635,NH,NEW HAMPSHIRE +636,MI,MICHIGAN +637,NY,NEW YORK +639,TX,TEXAS +641,OK,OKLAHOMA +642,OH,OHIO +643,NJ,NEW JERSEY +644,AR,ARKANSAS +645,MH,MARSHALL ISLANDS +646,IN,INDIANA +647,KY,KENTUCKY +648,VA,VIRGINIA +649,MA,MASSACHUSETTS +650,MT,MONTANA +651,FM,FEDERATED STATES OF MICRONESIA +652,WI,WISCONSIN +653,DE,DELAWARE +654,IA,IOWA +655,AZ,ARIZONA +656,AL,ALABAMA +657,NY,NEW YORK +658,VI,VIRGIN ISLANDS +659,DE,DELAWARE +660,MH,MARSHALL ISLANDS +661,CT,CONNECTICUT +662,FL,FLORIDA +663,CT,CONNECTICUT +664,MH,MARSHALL ISLANDS +665,IN,INDIANA +666,ME,MAINE +667,CO,COLORADO +668,NE,NEBRASKA +669,WI,WISCONSIN +670,NY,NEW YORK +671,MI,MICHIGAN +672,HI,HAWAII +673,undefined,undefined +674,RI,RHODE ISLAND +675,MA,MASSACHUSETTS +676,DC,DISTRICT OF COLUMBIA +677,VA,VIRGINIA +678,ID,IDAHO +679,OH,OHIO +680,AR,ARKANSAS +681,FL,FLORIDA +682,MO,MISSOURI +683,RI,RHODE ISLAND +684,WA,WASHINGTON +685,IL,ILLINOIS +686,PR,PUERTO RICO +687,PR,PUERTO RICO +688,MO,MISSOURI +689,AK,ALASKA +690,OR,OREGON +691,HI,HAWAII +692,IL,ILLINOIS +693,DE,DELAWARE +694,NH,NEW HAMPSHIRE +695,WA,WASHINGTON +696,UT,UTAH +697,DE,DELAWARE +698,KY,KENTUCKY +699,NJ,NEW JERSEY +700,VT,VERMONT +701,MO,MISSOURI +702,FM,FEDERATED STATES OF MICRONESIA +703,AL,ALABAMA +704,PA,PENNSYLVANIA +705,GA,GEORGIA +706,SD,SOUTH DAKOTA +707,KY,KENTUCKY +708,IN,INDIANA +709,IN,INDIANA +710,IN,INDIANA +711,IA,IOWA +712,AZ,ARIZONA +713,KY,KENTUCKY +714,WI,WISCONSIN +715,TN,TENNESSEE +716,VA,VIRGINIA +717,AR,ARKANSAS +718,NE,NEBRASKA +719,PW,PALAU +720,VT,VERMONT +721,MD,MARYLAND +722,RI,RHODE ISLAND +723,PW,PALAU +724,IL,ILLINOIS +725,KY,KENTUCKY +726,AZ,ARIZONA +727,GU,GUAM +728,VA,VIRGINIA +729,WV,WEST VIRGINIA +730,WV,WEST VIRGINIA +731,CA,CALIFORNIA +732,AK,ALASKA +733,CT,CONNECTICUT +734,HI,HAWAII +735,ID,IDAHO +736,GU,GUAM +737,PA,PENNSYLVANIA +738,undefined,undefined +739,MA,MASSACHUSETTS +741,UT,UTAH +742,WI,WISCONSIN +743,KS,KANSAS +744,NE,NEBRASKA +745,IN,INDIANA +746,RI,RHODE ISLAND +747,KY,KENTUCKY +748,FL,FLORIDA +749,HI,HAWAII +750,LA,LOUISIANA +751,DE,DELAWARE +752,TN,TENNESSEE +753,AS,AMERICAN SAMOA +754,NY,NEW YORK +755,TN,TENNESSEE +756,CA,CALIFORNIA +757,OK,OKLAHOMA +758,WY,WYOMING +759,IN,INDIANA +760,DE,DELAWARE +761,OH,OHIO +762,RI,RHODE ISLAND +763,HI,HAWAII +764,OK,OKLAHOMA +765,FL,FLORIDA +766,ME,MAINE +767,WV,WEST VIRGINIA +768,DE,DELAWARE +769,undefined,undefined +770,MN,MINNESOTA +771,DC,DISTRICT OF COLUMBIA +772,MD,MARYLAND +773,GU,GUAM +774,VA,VIRGINIA +775,MO,MISSOURI +776,PR,PUERTO RICO +777,VI,VIRGIN ISLANDS +778,MT,MONTANA +779,PA,PENNSYLVANIA +780,MD,MARYLAND +781,NV,NEVADA +782,OK,OKLAHOMA +783,HI,HAWAII +784,AS,AMERICAN SAMOA +785,WY,WYOMING +786,ME,MAINE +787,TX,TEXAS +788,IA,IOWA +789,VA,VIRGINIA +790,AZ,ARIZONA +791,MD,MARYLAND +792,DC,DISTRICT OF COLUMBIA +793,PR,PUERTO RICO +794,GA,GEORGIA +795,FM,FEDERATED STATES OF MICRONESIA +796,SD,SOUTH DAKOTA +797,RI,RHODE ISLAND +798,OH,OHIO +799,ID,IDAHO +800,AR,ARKANSAS +801,MO,MISSOURI +802,CT,CONNECTICUT +803,MT,MONTANA +804,CO,COLORADO +805,PR,PUERTO RICO +806,MS,MISSISSIPPI +807,CA,CALIFORNIA +808,GU,GUAM +809,WI,WISCONSIN +810,RI,RHODE ISLAND +811,NV,NEVADA +812,NY,NEW YORK +814,WA,WASHINGTON +815,NJ,NEW JERSEY +816,PW,PALAU +817,AR,ARKANSAS +818,GU,GUAM +819,SD,SOUTH DAKOTA +820,MO,MISSOURI +821,MO,MISSOURI +822,NV,NEVADA +823,ID,IDAHO +824,AR,ARKANSAS +825,DE,DELAWARE +827,IL,ILLINOIS +828,ID,IDAHO +829,MH,MARSHALL ISLANDS +830,NM,NEW MEXICO +831,MH,MARSHALL ISLANDS +832,MD,MARYLAND +833,AK,ALASKA +834,HI,HAWAII +835,GA,GEORGIA +836,NE,NEBRASKA +837,DE,DELAWARE +838,MA,MASSACHUSETTS +839,GU,GUAM +840,PW,PALAU +841,NH,NEW HAMPSHIRE +842,NJ,NEW JERSEY +843,OK,OKLAHOMA +844,WY,WYOMING +845,OR,OREGON +846,FM,FEDERATED STATES OF MICRONESIA +847,PW,PALAU +848,ME,MAINE +849,OR,OREGON +850,VT,VERMONT +851,AL,ALABAMA +852,NY,NEW YORK +854,SD,SOUTH DAKOTA +855,PW,PALAU +856,WI,WISCONSIN +857,TX,TEXAS +858,WA,WASHINGTON +859,ID,IDAHO +860,PA,PENNSYLVANIA +861,PW,PALAU +862,AK,ALASKA +863,TN,TENNESSEE +864,SC,SOUTH CAROLINA +865,PW,PALAU +866,SD,SOUTH DAKOTA +868,KS,KANSAS +869,KY,KENTUCKY +870,IL,ILLINOIS +871,WA,WASHINGTON +872,AL,ALABAMA +873,OK,OKLAHOMA +874,AK,ALASKA +875,ME,MAINE +876,GU,GUAM +877,AK,ALASKA +878,AZ,ARIZONA +879,TN,TENNESSEE +880,MD,MARYLAND +881,WI,WISCONSIN +882,MN,MINNESOTA +883,OK,OKLAHOMA +884,TN,TENNESSEE +885,IL,ILLINOIS +886,UT,UTAH +887,OR,OREGON +888,OH,OHIO +889,UT,UTAH +890,TN,TENNESSEE +891,MN,MINNESOTA +892,ME,MAINE +893,GA,GEORGIA +894,MI,MICHIGAN +895,HI,HAWAII +896,TX,TEXAS +897,FL,FLORIDA +898,OK,OKLAHOMA +899,MN,MINNESOTA +900,ID,IDAHO +901,SC,SOUTH CAROLINA +902,NJ,NEW JERSEY +903,RI,RHODE ISLAND +904,IA,IOWA +905,PA,PENNSYLVANIA +906,GU,GUAM +907,LA,LOUISIANA +908,PW,PALAU +909,UT,UTAH +910,RI,RHODE ISLAND +911,AS,AMERICAN SAMOA +912,CA,CALIFORNIA +913,OK,OKLAHOMA +914,IA,IOWA +915,AK,ALASKA +916,VI,VIRGIN ISLANDS +917,WI,WISCONSIN +918,GA,GEORGIA +919,CA,CALIFORNIA +920,NY,NEW YORK +921,VI,VIRGIN ISLANDS +922,WA,WASHINGTON +923,MO,MISSOURI +924,NV,NEVADA +925,VT,VERMONT +926,CT,CONNECTICUT +927,MT,MONTANA +928,ME,MAINE +929,WA,WASHINGTON +930,MH,MARSHALL ISLANDS +931,IA,IOWA +932,UT,UTAH +933,WI,WISCONSIN +934,LA,LOUISIANA +935,TX,TEXAS +937,ME,MAINE +938,MA,MASSACHUSETTS +939,FM,FEDERATED STATES OF MICRONESIA +940,NY,NEW YORK +941,NV,NEVADA +942,WV,WEST VIRGINIA +944,AZ,ARIZONA +945,WI,WISCONSIN +946,NV,NEVADA +947,RI,RHODE ISLAND +948,MD,MARYLAND +949,OH,OHIO +950,MN,MINNESOTA +951,MI,MICHIGAN +952,IL,ILLINOIS +953,UT,UTAH +954,MO,MISSOURI +955,IA,IOWA +956,WA,WASHINGTON +957,NJ,NEW JERSEY +958,MA,MASSACHUSETTS +959,PA,PENNSYLVANIA +960,GA,GEORGIA +961,WV,WEST VIRGINIA +962,MH,MARSHALL ISLANDS +963,VA,VIRGINIA +964,VT,VERMONT +965,TN,TENNESSEE +966,MI,MICHIGAN +967,AZ,ARIZONA +968,AS,AMERICAN SAMOA +969,NH,NEW HAMPSHIRE +970,GU,GUAM +971,NV,NEVADA +972,OR,OREGON +973,SD,SOUTH DAKOTA +974,FL,FLORIDA +975,UT,UTAH +976,ME,MAINE +977,KS,KANSAS +978,GU,GUAM +979,ID,IDAHO +980,WV,WEST VIRGINIA +981,OH,OHIO +982,undefined,undefined +983,TX,TEXAS +984,OR,OREGON +985,MA,MASSACHUSETTS +986,DC,DISTRICT OF COLUMBIA +987,LA,LOUISIANA +988,DE,DELAWARE +989,WI,WISCONSIN +990,KS,KANSAS +991,OK,OKLAHOMA +992,NE,NEBRASKA +993,undefined,undefined +994,MD,MARYLAND +995,TN,TENNESSEE +996,OH,OHIO +997,CA,CALIFORNIA +998,MI,MICHIGAN +999,TX,TEXAS +1000,NE,NEBRASKA diff --git a/integration-tests/spark-native/datasets/0005-filter-true-golden.csv b/integration-tests/spark-native/datasets/0005-filter-true-golden.csv new file mode 100644 index 00000000000..490385b0e67 --- /dev/null +++ b/integration-tests/spark-native/datasets/0005-filter-true-golden.csv @@ -0,0 +1,48 @@ +id,stateCode,state +9,NC,NORTH CAROLINA +23,NC,NORTH CAROLINA +49,MP,NORTHERN MARIANA ISLANDS +52,NC,NORTH CAROLINA +53,NC,NORTH CAROLINA +73,ND,NORTH DAKOTA +74,ND,NORTH DAKOTA +102,MP,NORTHERN MARIANA ISLANDS +105,NC,NORTH CAROLINA +119,MP,NORTHERN MARIANA ISLANDS +136,MP,NORTHERN MARIANA ISLANDS +149,ND,NORTH DAKOTA +159,NC,NORTH CAROLINA +184,MP,NORTHERN MARIANA ISLANDS +195,NC,NORTH CAROLINA +200,ND,NORTH DAKOTA +229,MP,NORTHERN MARIANA ISLANDS +241,MP,NORTHERN MARIANA ISLANDS +264,ND,NORTH DAKOTA +291,ND,NORTH DAKOTA +299,NC,NORTH CAROLINA +319,MP,NORTHERN MARIANA ISLANDS +324,MP,NORTHERN MARIANA ISLANDS +334,ND,NORTH DAKOTA +381,NC,NORTH CAROLINA +393,NC,NORTH CAROLINA +444,MP,NORTHERN MARIANA ISLANDS +451,NC,NORTH CAROLINA +466,NC,NORTH CAROLINA +472,ND,NORTH DAKOTA +525,ND,NORTH DAKOTA +529,ND,NORTH DAKOTA +537,ND,NORTH DAKOTA +575,NC,NORTH CAROLINA +580,NC,NORTH CAROLINA +587,MP,NORTHERN MARIANA ISLANDS +604,MP,NORTHERN MARIANA ISLANDS +609,ND,NORTH DAKOTA +638,NC,NORTH CAROLINA +640,ND,NORTH DAKOTA +740,MP,NORTHERN MARIANA ISLANDS +813,ND,NORTH DAKOTA +826,NC,NORTH CAROLINA +853,MP,NORTHERN MARIANA ISLANDS +867,MP,NORTHERN MARIANA ISLANDS +936,MP,NORTHERN MARIANA ISLANDS +943,MP,NORTHERN MARIANA ISLANDS diff --git a/integration-tests/spark-native/datasets/0006-switch-ca-golden.csv b/integration-tests/spark-native/datasets/0006-switch-ca-golden.csv new file mode 100644 index 00000000000..a68c4828031 --- /dev/null +++ b/integration-tests/spark-native/datasets/0006-switch-ca-golden.csv @@ -0,0 +1,20 @@ +id,stateCode +12,CA +25,CA +26,CA +44,CA +135,CA +177,CA +181,CA +272,CA +304,CA +308,CA +422,CA +561,CA +600,CA +731,CA +756,CA +807,CA +912,CA +919,CA +997,CA diff --git a/integration-tests/spark-native/datasets/0006-switch-default-golden.csv b/integration-tests/spark-native/datasets/0006-switch-default-golden.csv new file mode 100644 index 00000000000..cb8d56df93b --- /dev/null +++ b/integration-tests/spark-native/datasets/0006-switch-default-golden.csv @@ -0,0 +1,941 @@ +id,stateCode +1,AK +2,GA +3,NJ +5,IN +6,MD +7,PA +8,PW +9,NC +10,AR +11,NH +13,OK +14,MT +15,AS +16,MA +17,KY +18,WI +19,ME +20,MD +21,TN +22,VI +23,NC +24,WA +27,MO +28,NV +29,MS +30,GA +31,OK +32,SD +33,IN +34,NH +35,MS +36,NV +37,PW +38,undefined +39,OK +40,DC +41,CO +42,AK +43,VI +45,TX +46,PW +47,UT +48,WV +49,MP +50,LA +51,GA +52,NC +53,NC +54,MO +55,KS +56,HI +57,GU +58,DC +60,RI +61,SD +62,NV +63,AS +64,OK +65,FM +67,VA +68,PW +69,WA +71,ID +72,MN +73,ND +74,ND +75,VA +76,CO +77,VT +78,MD +79,NM +80,LA +81,GA +82,GU +83,TX +84,IN +85,NV +86,CT +87,ME +88,SD +89,DC +90,TX +91,ME +92,WV +93,NV +94,MO +95,MA +97,KS +98,RI +99,MD +100,NM +101,WI +102,MP +103,MI +104,OK +105,NC +106,MO +107,AZ +108,MH +109,GA +110,HI +111,AK +112,AL +113,GA +114,MH +115,KS +116,WV +117,OK +118,NV +119,MP +120,WA +121,ID +123,VT +124,AK +126,WI +127,MO +128,AS +129,IN +130,UT +131,OR +132,NE +133,WV +134,TX +136,MP +137,MT +138,WY +139,SD +140,LA +141,NJ +142,NE +143,HI +144,MT +145,NH +146,DE +147,MN +148,NH +149,ND +150,DC +151,IN +152,AR +153,MA +155,MT +156,UT +158,WY +159,NC +160,NJ +161,NJ +162,NJ +163,LA +164,MA +165,CO +166,KY +167,CO +168,KS +169,OR +170,MN +171,UT +172,NE +173,MS +174,WY +175,KY +176,SC +179,PR +180,AZ +182,NE +183,PR +184,MP +185,MS +186,MO +187,AR +188,DE +189,OK +190,IN +191,UT +192,WA +193,AS +194,KY +195,NC +196,MH +197,VA +198,MT +199,CO +200,ND +201,MS +203,PA +204,VA +205,NM +206,NV +207,UT +208,GU +209,PA +210,KS +211,VI +213,TX +214,MA +215,NJ +216,KS +217,NV +218,AK +219,RI +222,HI +223,SD +224,VT +225,LA +226,VT +227,FM +228,MT +229,MP +230,LA +231,KY +232,NM +233,WA +234,MI +235,WI +236,DC +237,NM +238,MN +239,PA +240,PR +241,MP +242,WV +243,MI +244,undefined +245,AR +246,ME +247,AL +248,IN +249,MD +250,IL +251,DC +252,TX +253,IN +254,AR +255,MH +256,DC +257,VT +258,CO +259,GU +260,MD +261,AZ +262,MT +263,AK +264,ND +265,OK +266,MO +267,MT +268,ME +270,KS +271,GA +273,PR +274,OK +275,MN +276,MN +277,ME +278,VI +279,HI +280,CO +281,CO +282,WV +283,MH +284,UT +285,ME +286,WA +287,PA +288,MN +289,VA +290,WV +291,ND +292,CO +293,VT +294,LA +295,NH +297,WV +298,VI +299,NC +300,WA +301,WV +302,HI +303,NM +305,VT +306,TX +307,PA +309,UT +310,PR +311,WY +312,NE +313,undefined +314,IL +315,VI +316,NH +317,DC +318,GU +319,MP +321,AR +322,WY +323,AZ +324,MP +325,AL +326,VT +327,KY +328,VA +329,CT +330,PW +331,OR +332,GU +333,SD +334,ND +335,AR +336,SD +337,WA +338,SD +339,AZ +340,WI +341,SC +342,IA +343,PW +344,AL +345,NE +347,SD +348,undefined +349,undefined +350,VI +351,AR +352,KY +353,IN +354,MT +355,HI +356,NE +357,UT +358,PR +359,MS +360,MA +361,CT +362,CT +363,MI +364,MT +365,AS +366,MI +367,ID +368,IL +369,DC +370,RI +371,KS +372,VA +373,NE +374,LA +375,MS +376,HI +377,WY +378,HI +379,MN +380,AS +381,NC +382,WY +383,VI +384,TX +385,VA +386,AL +387,WY +388,undefined +390,NE +391,ME +392,DC +393,NC +394,RI +395,NH +396,MN +397,UT +398,CT +399,AZ +400,AR +401,SC +402,NV +403,NV +404,WY +405,CT +406,MO +407,MN +408,VT +409,WV +410,AZ +411,MN +412,AS +413,LA +414,VA +415,NE +416,ME +417,DE +418,SC +419,OK +420,KS +421,GA +423,MO +424,NJ +425,SC +426,GU +427,OK +428,MH +429,NV +430,PA +431,NV +432,HI +433,AR +434,KS +435,AR +436,OK +437,LA +438,SD +439,WV +440,PR +441,OK +442,WV +443,NM +444,MP +445,VA +446,MN +447,AS +448,NV +449,CO +450,MA +451,NC +452,DE +453,ME +454,WA +456,MN +457,SD +458,PW +459,MD +460,MT +461,NJ +462,FM +463,KY +464,MD +465,NM +466,NC +467,MD +468,AK +469,VI +470,LA +472,ND +473,WY +474,FM +475,OR +476,WY +477,MT +478,UT +480,MT +481,DE +482,AK +483,MN +484,MN +485,RI +486,LA +487,WV +488,undefined +489,DE +490,IN +491,HI +492,NV +493,MT +494,WY +495,OH +496,GU +497,HI +498,IL +499,MO +500,IA +501,DE +502,MT +503,AZ +504,AR +505,AS +506,OH +507,WY +508,NH +509,RI +510,MD +511,AR +512,OR +513,MN +514,MA +515,WV +516,OK +517,IA +518,NE +519,AL +520,MS +521,KY +522,ID +523,WV +524,LA +525,ND +526,NV +527,IN +528,MN +529,ND +530,SD +531,ID +532,MN +533,FM +534,FM +535,IN +536,AK +537,ND +538,NJ +539,KY +540,AR +541,WI +543,CO +544,AL +545,KS +546,KS +547,CO +548,VA +549,PA +550,RI +551,SD +552,CO +553,IA +554,NV +555,AK +556,DC +557,NH +558,NE +559,IL +560,VA +562,VA +563,CO +564,DE +565,AL +566,LA +567,SC +568,OH +569,GU +570,FM +571,AL +573,MS +574,NJ +575,NC +576,AK +577,NH +578,KS +579,NE +580,NC +581,KS +582,TN +583,AK +584,WA +585,IA +586,VT +587,MP +588,CO +589,undefined +590,WV +591,VT +592,IN +593,HI +594,IL +595,MA +596,CT +597,LA +598,OH +601,NE +602,IA +603,PA +604,MP +605,MA +606,OR +607,RI +608,AL +609,ND +610,RI +611,LA +612,TX +613,CT +614,MS +615,KY +616,AS +617,NM +618,NJ +619,HI +620,OK +621,MA +622,OK +623,AS +624,CO +625,LA +626,CT +628,NV +629,ID +631,MH +632,DC +633,IA +634,VI +635,NH +636,MI +638,NC +639,TX +640,ND +641,OK +642,OH +643,NJ +644,AR +645,MH +646,IN +647,KY +648,VA +649,MA +650,MT +651,FM +652,WI +653,DE +654,IA +655,AZ +656,AL +658,VI +659,DE +660,MH +661,CT +663,CT +664,MH +665,IN +666,ME +667,CO +668,NE +669,WI +671,MI +672,HI +673,undefined +674,RI +675,MA +676,DC +677,VA +678,ID +679,OH +680,AR +682,MO +683,RI +684,WA +685,IL +686,PR +687,PR +688,MO +689,AK +690,OR +691,HI +692,IL +693,DE +694,NH +695,WA +696,UT +697,DE +698,KY +699,NJ +700,VT +701,MO +702,FM +703,AL +704,PA +705,GA +706,SD +707,KY +708,IN +709,IN +710,IN +711,IA +712,AZ +713,KY +714,WI +715,TN +716,VA +717,AR +718,NE +719,PW +720,VT +721,MD +722,RI +723,PW +724,IL +725,KY +726,AZ +727,GU +728,VA +729,WV +730,WV +732,AK +733,CT +734,HI +735,ID +736,GU +737,PA +738,undefined +739,MA +740,MP +741,UT +742,WI +743,KS +744,NE +745,IN +746,RI +747,KY +749,HI +750,LA +751,DE +752,TN +753,AS +755,TN +757,OK +758,WY +759,IN +760,DE +761,OH +762,RI +763,HI +764,OK +766,ME +767,WV +768,DE +769,undefined +770,MN +771,DC +772,MD +773,GU +774,VA +775,MO +776,PR +777,VI +778,MT +779,PA +780,MD +781,NV +782,OK +783,HI +784,AS +785,WY +786,ME +787,TX +788,IA +789,VA +790,AZ +791,MD +792,DC +793,PR +794,GA +795,FM +796,SD +797,RI +798,OH +799,ID +800,AR +801,MO +802,CT +803,MT +804,CO +805,PR +806,MS +808,GU +809,WI +810,RI +811,NV +813,ND +814,WA +815,NJ +816,PW +817,AR +818,GU +819,SD +820,MO +821,MO +822,NV +823,ID +824,AR +825,DE +826,NC +827,IL +828,ID +829,MH +830,NM +831,MH +832,MD +833,AK +834,HI +835,GA +836,NE +837,DE +838,MA +839,GU +840,PW +841,NH +842,NJ +843,OK +844,WY +845,OR +846,FM +847,PW +848,ME +849,OR +850,VT +851,AL +853,MP +854,SD +855,PW +856,WI +857,TX +858,WA +859,ID +860,PA +861,PW +862,AK +863,TN +864,SC +865,PW +866,SD +867,MP +868,KS +869,KY +870,IL +871,WA +872,AL +873,OK +874,AK +875,ME +876,GU +877,AK +878,AZ +879,TN +880,MD +881,WI +882,MN +883,OK +884,TN +885,IL +886,UT +887,OR +888,OH +889,UT +890,TN +891,MN +892,ME +893,GA +894,MI +895,HI +896,TX +898,OK +899,MN +900,ID +901,SC +902,NJ +903,RI +904,IA +905,PA +906,GU +907,LA +908,PW +909,UT +910,RI +911,AS +913,OK +914,IA +915,AK +916,VI +917,WI +918,GA +921,VI +922,WA +923,MO +924,NV +925,VT +926,CT +927,MT +928,ME +929,WA +930,MH +931,IA +932,UT +933,WI +934,LA +935,TX +936,MP +937,ME +938,MA +939,FM +941,NV +942,WV +943,MP +944,AZ +945,WI +946,NV +947,RI +948,MD +949,OH +950,MN +951,MI +952,IL +953,UT +954,MO +955,IA +956,WA +957,NJ +958,MA +959,PA +960,GA +961,WV +962,MH +963,VA +964,VT +965,TN +966,MI +967,AZ +968,AS +969,NH +970,GU +971,NV +972,OR +973,SD +975,UT +976,ME +977,KS +978,GU +979,ID +980,WV +981,OH +982,undefined +983,TX +984,OR +985,MA +986,DC +987,LA +988,DE +989,WI +990,KS +991,OK +992,NE +993,undefined +994,MD +995,TN +996,OH +998,MI +999,TX +1000,NE diff --git a/integration-tests/spark-native/datasets/0006-switch-fl-golden.csv b/integration-tests/spark-native/datasets/0006-switch-fl-golden.csv new file mode 100644 index 00000000000..1bab182fa27 --- /dev/null +++ b/integration-tests/spark-native/datasets/0006-switch-fl-golden.csv @@ -0,0 +1,21 @@ +id,stateCode +59,FL +70,FL +96,FL +125,FL +178,FL +202,FL +212,FL +269,FL +296,FL +320,FL +346,FL +389,FL +479,FL +630,FL +662,FL +681,FL +748,FL +765,FL +897,FL +974,FL diff --git a/integration-tests/spark-native/datasets/0006-switch-ny-golden.csv b/integration-tests/spark-native/datasets/0006-switch-ny-golden.csv new file mode 100644 index 00000000000..27c83170e23 --- /dev/null +++ b/integration-tests/spark-native/datasets/0006-switch-ny-golden.csv @@ -0,0 +1,22 @@ +id,stateCode +4,NY +66,NY +122,NY +154,NY +157,NY +220,NY +221,NY +455,NY +471,NY +542,NY +572,NY +599,NY +627,NY +637,NY +657,NY +670,NY +754,NY +812,NY +852,NY +920,NY +940,NY diff --git a/integration-tests/spark-native/datasets/0008-excel-stream-lookup-golden.csv b/integration-tests/spark-native/datasets/0008-excel-stream-lookup-golden.csv new file mode 100644 index 00000000000..ba409626c13 --- /dev/null +++ b/integration-tests/spark-native/datasets/0008-excel-stream-lookup-golden.csv @@ -0,0 +1,7 @@ +orderId,productCode,qty,productName,unitPrice +1,AAA,2,Alpha Widget,10 +2,BBB,1,Beta Gadget,20 +3,CCC,5,Gamma Device,30 +4,AAA,1,Alpha Widget,10 +5,BBB,3,Beta Gadget,20 +6,CCC,2,Gamma Device,30 diff --git a/integration-tests/spark-native/datasets/0099-complex-golden.csv b/integration-tests/spark-native/datasets/0099-complex-golden.csv new file mode 100644 index 00000000000..1733724b107 --- /dev/null +++ b/integration-tests/spark-native/datasets/0099-complex-golden.csv @@ -0,0 +1,2 @@ +id,Last name,First name,cust_zip_code,city,birthdate,street,housenr,stateCode,state,state_1,population,nrPerState,label,Comment +,,,,,,,,,,,,,, diff --git a/integration-tests/spark-native/datasets/0099-customers-input.csv b/integration-tests/spark-native/datasets/0099-customers-input.csv new file mode 100644 index 00000000000..70c8908f5f2 --- /dev/null +++ b/integration-tests/spark-native/datasets/0099-customers-input.csv @@ -0,0 +1,1002 @@ +id,lastName,firstName,cust_zip_code,city,birthdate,street,housenr,stateCode,state +1,jwcdf-name,fsj-firstname, 13520,oem-city,1954/02/07,amrb-street, 145,AK,ALASKA +2,flhxu-name,tum-firstname, 17520,buo-city,1966/04/24,wfyz-street, 96,GA,GEORGIA +3,xthfg-name,gfe-firstname, 12560,vtz-city,1990/01/11,doxx-street, 46,NJ,NEW JERSEY +4,ulzrz-name,bnl-firstname, 11620,prz-city,1966/08/02,bxqn-street, 104,NY,NEW YORK +5,oxhyr-name,onx-firstname, 15180,bpn-city,1970/11/14,pksn-street, 133,IN,INDIANA +6,fiqjz-name,sce-firstname, 16020,fnn-city,1954/09/24,wbhg-street, 35,MD,MARYLAND +7,tkiat-name,xti-firstname, 12720,stt-city,1966/08/11,tvnf-street, 21,PA,PENNSYLVANIA +8,kljcz-name,uqd-firstname, 13340,ntt-city,1987/01/15,jyje-street, 10,PW,PALAU +9,pgunz-name,hcm-firstname, 16680,gxh-city,1970/11/08,shbe-street, 184,NC,NORTH CAROLINA +10,oyjha-name,uhj-firstname, 18880,uyg-city,1966/04/10,bjgw-street, 176,AR,ARKANSAS +11,igxbd-name,uph-firstname, 13480,ndh-city,1962/12/03,jdcd-street, 151,NH,NEW HAMPSHIRE +12,vnaov-name,wha-firstname, 13120,egm-city,1954/03/28,hpep-street, 20,CA,CALIFORNIA +13,dauuz-name,hwg-firstname, 13740,khn-city,1958/05/15,etqx-street, 5,OK,OKLAHOMA +14,gkuuo-name,kkb-firstname, 13560,xdt-city,1962/04/07,sdoj-street, 35,MT,MONTANA +15,wdhze-name,jjk-firstname, 16900,due-city,1970/07/17,pmmu-street, 174,AS,AMERICAN SAMOA +16,ncayz-name,ynb-firstname, 15720,lxj-city,1974/04/27,mdtb-street, 109,MA,MASSACHUSETTS +17,rdjin-name,hhu-firstname, 14480,lpc-city,1958/11/16,wxik-street, 145,KY,KENTUCKY +18,nxzij-name,bdl-firstname, 10740,avx-city,1958/02/20,nybz-street, 138,WI,WISCONSIN +19,xgrzc-name,dxw-firstname, 18900,vpq-city,1990/11/16,wzjh-street, 58,ME,MAINE +20,ehgrn-name,vbe-firstname, 17500,cik-city,1978/05/21,ucnw-street, 135,MD,MARYLAND +21,gctjx-name,upx-firstname, 11960,yqr-city,1958/03/03,rlko-street, 141,TN,TENNESSEE +22,ptzmg-name,hva-firstname, 15740,gux-city,1978/05/04,pugy-street, 122,VI,VIRGIN ISLANDS +23,eyeti-name,gnw-firstname, 17420,eko-city,1962/10/26,ylph-street, 61,NC,NORTH CAROLINA +24,wccwo-name,zpj-firstname, 16600,uim-city,1962/09/29,ygih-street, 26,WA,WASHINGTON +25,bwkoe-name,ayl-firstname, 18660,rtw-city,1978/07/16,mzww-street, 179,CA,CALIFORNIA +26,rezku-name,zio-firstname, 19080,nvt-city,1982/07/14,wwkd-street, 91,CA,CALIFORNIA +27,mjlsk-name,ecx-firstname, 10800,yxu-city,1950/12/11,vttb-street, 195,MO,MISSOURI +28,wdjsi-name,aoq-firstname, 13660,smo-city,1954/02/01,kako-street, 7,NV,NEVADA +29,mwfnd-name,nyb-firstname, 19760,bbu-city,1986/09/23,apdi-street, 91,MS,MISSISSIPPI +30,vtuoz-name,jhh-firstname, 17620,vad-city,1982/05/05,kzup-street, 79,GA,GEORGIA +31,rhhxk-name,ndr-firstname, 16760,fub-city,1978/11/12,regd-street, 55,OK,OKLAHOMA +32,lpstk-name,mqz-firstname, 18940,tnr-city,1982/09/16,cdhf-street, 4,SD,SOUTH DAKOTA +33,ldhyr-name,yts-firstname, 12000,auk-city,1986/11/14,abph-street, 147,IN,INDIANA +34,cjdml-name,iti-firstname, 16900,wkq-city,1970/06/05,npow-street, 96,NH,NEW HAMPSHIRE +35,cpenz-name,sbi-firstname, 16380,ssl-city,1962/08/19,kilz-street, 44,MS,MISSISSIPPI +36,rxtbg-name,anr-firstname, 14720,bqc-city,1958/08/10,pudg-street, 140,NV,NEVADA +37,udblf-name,raa-firstname, 11500,wli-city,1978/12/13,xomd-street, 41,PW,PALAU +38,vvyce-name,gep-firstname, 13740,gtd-city,1982/05/23,kwbv-street, 123,undefined,undefined +39,kwfnz-name,ucu-firstname, 10580,sns-city,1978/08/18,nnun-street, 20,OK,OKLAHOMA +40,zxydx-name,tml-firstname, 14680,jda-city,1974/05/29,wfjn-street, 157,DC,DISTRICT OF COLUMBIA +41,bfscx-name,jnl-firstname, 16920,yyg-city,1970/11/30,cgfh-street, 178,CO,COLORADO +42,qitur-name,yra-firstname, 15560,ijp-city,1978/01/30,fonc-street, 155,AK,ALASKA +43,msixi-name,ynb-firstname, 12720,ksl-city,1958/07/17,zpjw-street, 46,VI,VIRGIN ISLANDS +44,wzkjq-name,rgh-firstname, 19000,hkm-city,1974/08/12,yixf-street, 134,CA,CALIFORNIA +45,dqfmf-name,yxr-firstname, 13840,vie-city,1962/10/23,stvx-street, 39,TX,TEXAS +46,biluz-name,uqe-firstname, 17760,wkq-city,1962/07/27,embn-street, 183,PW,PALAU +47,wahfx-name,zwd-firstname, 13240,vic-city,1974/03/27,axpw-street, 131,UT,UTAH +48,denwt-name,bta-firstname, 17300,hhj-city,1986/12/20,orwy-street, 11,WV,WEST VIRGINIA +49,akdmy-name,ybz-firstname, 14560,wtx-city,1962/11/08,nwba-street, 123,MP,NORTHERN MARIANA ISLANDS +50,hqafg-name,nht-firstname, 16080,gfu-city,1951/01/12,spsq-street, 45,LA,LOUISIANA +51,zhmbl-name,lnw-firstname, 17460,hse-city,1986/12/21,scis-street, 97,GA,GEORGIA +52,snwnj-name,jyy-firstname, 16400,hsz-city,1966/02/15,imhl-street, 42,NC,NORTH CAROLINA +53,fuyla-name,mmp-firstname, 11840,hgu-city,1986/08/16,ixiz-street, 145,NC,NORTH CAROLINA +54,yvfqz-name,prz-firstname, 11260,wjl-city,1982/05/06,fbzd-street, 97,MO,MISSOURI +55,usbgq-name,vhd-firstname, 14080,dsb-city,1958/04/01,ggoc-street, 54,KS,KANSAS +56,yaeni-name,zpy-firstname, 19100,sen-city,1954/12/10,sbsw-street, 158,HI,HAWAII +57,fgxvr-name,vzi-firstname, 17520,lcf-city,1958/11/01,nbdv-street, 10,GU,GUAM +58,tqpbq-name,rwr-firstname, 19140,zpd-city,1978/08/23,npvb-street, 190,DC,DISTRICT OF COLUMBIA +59,ieigg-name,ayq-firstname, 12960,ljc-city,1962/07/05,dnjz-street, 163,FL,FLORIDA +60,rfvzu-name,edm-firstname, 13340,kvz-city,1954/12/08,eijd-street, 4,RI,RHODE ISLAND +61,pduwm-name,gqb-firstname, 14240,cyr-city,1954/07/03,ndux-street, 13,SD,SOUTH DAKOTA +62,yyixf-name,yzt-firstname, 18020,lwx-city,1974/01/29,iede-street, 120,NV,NEVADA +63,dkszq-name,ytd-firstname, 14700,zwh-city,1979/01/11,nbjz-street, 65,AS,AMERICAN SAMOA +64,slkzv-name,zbg-firstname, 19880,oee-city,1978/11/01,sphg-street, 119,OK,OKLAHOMA +65,nvxim-name,phc-firstname, 19220,vgg-city,1991/01/24,juok-street, 106,FM,FEDERATED STATES OF MICRONESIA +66,piyfg-name,xtn-firstname, 13760,nde-city,1954/07/22,vfrv-street, 11,NY,NEW YORK +67,jnusz-name,mjw-firstname, 12640,nwb-city,1986/08/23,kcsa-street, 138,VA,VIRGINIA +68,jnypj-name,ioq-firstname, 17000,zqy-city,1986/01/09,croe-street, 119,PW,PALAU +69,uohts-name,btx-firstname, 13480,dal-city,1990/10/22,llyw-street, 150,WA,WASHINGTON +70,aavpj-name,pvw-firstname, 13780,lai-city,1954/09/23,nygu-street, 171,FL,FLORIDA +71,nbjcj-name,rsf-firstname, 12000,kjl-city,1986/06/30,ijsb-street, 123,ID,IDAHO +72,syjxh-name,gkq-firstname, 19960,rmd-city,1978/10/26,qmyp-street, 161,MN,MINNESOTA +73,vkojz-name,ryo-firstname, 14300,bmz-city,1954/09/11,gcpj-street, 71,ND,NORTH DAKOTA +74,pqzfw-name,kld-firstname, 16400,qvq-city,1962/09/09,dhbv-street, 92,ND,NORTH DAKOTA +75,owvjk-name,fez-firstname, 19740,ldb-city,1978/06/14,kabf-street, 87,VA,VIRGINIA +76,qsfih-name,ixe-firstname, 16860,qvr-city,1987/01/07,qean-street, 159,CO,COLORADO +77,slixq-name,gmb-firstname, 19980,ftt-city,1982/06/22,xinx-street, 111,VT,VERMONT +78,eegsa-name,xlc-firstname, 12680,byk-city,1954/04/23,beul-street, 56,MD,MARYLAND +79,phevp-name,ihs-firstname, 16120,adc-city,1978/04/25,voig-street, 98,NM,NEW MEXICO +80,njfoe-name,tag-firstname, 16580,tnr-city,1966/12/04,dhky-street, 108,LA,LOUISIANA +81,bdncx-name,hcd-firstname, 11260,xcl-city,1970/07/02,jvlp-street, 49,GA,GEORGIA +82,ikedo-name,tks-firstname, 17460,odl-city,1958/08/25,iaaq-street, 8,GU,GUAM +83,iafxy-name,vur-firstname, 11480,hgt-city,1962/08/03,hmec-street, 164,TX,TEXAS +84,lafhf-name,ssz-firstname, 19560,wwp-city,1951/01/25,mxmq-street, 96,IN,INDIANA +85,okyny-name,hbu-firstname, 16800,yok-city,1978/03/28,ipjz-street, 135,NV,NEVADA +86,hznby-name,fwy-firstname, 13680,wbi-city,1970/07/25,mxui-street, 170,CT,CONNECTICUT +87,ztpoa-name,rzk-firstname, 18500,qum-city,1970/07/26,blqr-street, 152,ME,MAINE +88,gitxz-name,axt-firstname, 11800,fck-city,1974/01/12,tmjw-street, 189,SD,SOUTH DAKOTA +89,ziomm-name,mcv-firstname, 12940,iwq-city,1950/10/22,hqgj-street, 140,DC,DISTRICT OF COLUMBIA +90,otncg-name,tuy-firstname, 16540,ulk-city,1971/01/24,yuia-street, 166,TX,TEXAS +91,cnabb-name,hoq-firstname, 16300,tuw-city,1962/06/17,ujvv-street, 61,ME,MAINE +92,ucogf-name,ggc-firstname, 14500,fsj-city,1978/02/08,asfi-street, 53,WV,WEST VIRGINIA +93,lbpmf-name,sdt-firstname, 10780,ewj-city,1978/03/08,hxsp-street, 102,NV,NEVADA +94,tieqq-name,uyu-firstname, 17740,wea-city,1966/10/31,abpl-street, 187,MO,MISSOURI +95,fsgwf-name,vjd-firstname, 12460,ads-city,1970/11/29,yeou-street, 10,MA,MASSACHUSETTS +96,reeba-name,kzs-firstname, 13100,zhc-city,1966/07/08,abmv-street, 88,FL,FLORIDA +97,shybc-name,gcp-firstname, 10660,ahg-city,1950/12/15,hrqy-street, 174,KS,KANSAS +98,phszr-name,sst-firstname, 13080,ydd-city,1954/09/23,quqn-street, 2,RI,RHODE ISLAND +99,jteco-name,fxc-firstname, 19760,agr-city,1986/05/06,dzxc-street, 108,MD,MARYLAND +100,qvaar-name,icx-firstname, 16120,boc-city,1978/08/04,bfzf-street, 12,NM,NEW MEXICO +101,iiapu-name,veo-firstname, 10180,wdv-city,1954/05/29,ovyu-street, 55,WI,WISCONSIN +102,qfawh-name,wlx-firstname, 11280,fad-city,1962/04/28,hibx-street, 188,MP,NORTHERN MARIANA ISLANDS +103,nfurq-name,rib-firstname, 17080,xcp-city,1962/11/10,rqui-street, 164,MI,MICHIGAN +104,hdbiw-name,wxm-firstname, 12600,txy-city,1978/11/23,yfcx-street, 112,OK,OKLAHOMA +105,oyher-name,jws-firstname, 17900,bai-city,1978/12/30,tyil-street, 178,NC,NORTH CAROLINA +106,fzjnb-name,wxk-firstname, 12300,fda-city,1974/03/26,aweg-street, 7,MO,MISSOURI +107,ycpcp-name,xfq-firstname, 12660,mna-city,1986/02/14,dcki-street, 5,AZ,ARIZONA +108,rxxeb-name,qdw-firstname, 17600,yks-city,1970/11/15,zvsf-street, 74,MH,MARSHALL ISLANDS +109,ffbgl-name,fqf-firstname, 11680,npo-city,1974/12/07,vvan-street, 175,GA,GEORGIA +110,foygy-name,vog-firstname, 16920,mun-city,1970/07/03,urct-street, 153,HI,HAWAII +111,imoqe-name,xfe-firstname, 14620,gfv-city,1986/02/20,nlak-street, 181,AK,ALASKA +112,mnhwk-name,bbt-firstname, 16180,bnf-city,1986/10/10,yybd-street, 144,AL,ALABAMA +113,lkfyf-name,xhg-firstname, 13260,myb-city,1958/10/27,jjcn-street, 128,GA,GEORGIA +114,bkhya-name,hrh-firstname, 11500,byw-city,1990/03/03,oubr-street, 41,MH,MARSHALL ISLANDS +115,avceb-name,ztu-firstname, 10020,ogo-city,1974/05/26,iuob-street, 31,KS,KANSAS +116,pnacg-name,iws-firstname, 11640,dtx-city,1966/06/01,kwwj-street, 177,WV,WEST VIRGINIA +117,ypnsc-name,tyv-firstname, 16820,glg-city,1962/12/19,nysz-street, 13,OK,OKLAHOMA +118,agndn-name,qae-firstname, 15540,tpg-city,1990/05/15,ygzx-street, 166,NV,NEVADA +119,qqhon-name,tlb-firstname, 19880,iif-city,1982/02/05,vsbj-street, 191,MP,NORTHERN MARIANA ISLANDS +120,qzxic-name,mot-firstname, 14840,qvf-city,1970/03/19,gelo-street, 149,WA,WASHINGTON +121,lunca-name,ced-firstname, 13700,wjp-city,1979/01/06,racn-street, 54,ID,IDAHO +122,jsxhg-name,yoo-firstname, 13440,ulf-city,1982/06/28,krry-street, 55,NY,NEW YORK +123,ugbci-name,vht-firstname, 17100,ffc-city,1962/10/16,ixha-street, 99,VT,VERMONT +124,hgtkz-name,xgg-firstname, 17500,qck-city,1978/07/17,xkez-street, 60,AK,ALASKA +125,ejdoc-name,ovv-firstname, 14920,ocs-city,1954/11/15,bwhd-street, 169,FL,FLORIDA +126,zoucr-name,ivo-firstname, 14040,grt-city,1990/04/29,qhox-street, 90,WI,WISCONSIN +127,asofx-name,yzv-firstname, 19600,ixo-city,1982/07/09,slmy-street, 198,MO,MISSOURI +128,tgowi-name,zvm-firstname, 17560,avv-city,1986/11/04,qnyp-street, 83,AS,AMERICAN SAMOA +129,toeur-name,ydp-firstname, 17260,dpz-city,1962/11/02,atag-street, 60,IN,INDIANA +130,eohex-name,vfp-firstname, 17000,gzc-city,1970/01/05,cvlk-street, 188,UT,UTAH +131,mahci-name,cwt-firstname, 18240,wut-city,1982/08/02,zyse-street, 147,OR,OREGON +132,hzetq-name,kif-firstname, 14280,aep-city,1990/11/26,rrsr-street, 118,NE,NEBRASKA +133,tjrgp-name,vle-firstname, 15620,sdv-city,1962/12/08,zdyx-street, 88,WV,WEST VIRGINIA +134,japyi-name,jkm-firstname, 14960,jeo-city,1958/03/01,bsxv-street, 86,TX,TEXAS +135,eqley-name,ttv-firstname, 12740,trs-city,1974/09/16,ibff-street, 187,CA,CALIFORNIA +136,dbvkz-name,efr-firstname, 13700,ujo-city,1958/05/14,louh-street, 22,MP,NORTHERN MARIANA ISLANDS +137,mpgcz-name,ysk-firstname, 11860,eva-city,1978/08/23,nedx-street, 101,MT,MONTANA +138,bntsw-name,osn-firstname, 15700,mmv-city,1966/07/28,loqs-street, 34,WY,WYOMING +139,yfcbv-name,ing-firstname, 10300,ddb-city,1966/04/12,amuj-street, 32,SD,SOUTH DAKOTA +140,ddwqy-name,rxo-firstname, 18720,nsh-city,1974/06/22,rugn-street, 105,LA,LOUISIANA +141,zuxwq-name,xha-firstname, 12240,jii-city,1974/04/22,kawh-street, 97,NJ,NEW JERSEY +142,aarej-name,dfg-firstname, 14680,exu-city,1958/04/17,adua-street, 11,NE,NEBRASKA +143,iezfw-name,ufb-firstname, 18800,fyv-city,1970/06/05,yvao-street, 53,HI,HAWAII +144,vvmzr-name,bud-firstname, 15120,ggo-city,1966/07/24,ozcj-street, 127,MT,MONTANA +145,bknbv-name,qrd-firstname, 11500,mth-city,1970/04/16,ijle-street, 143,NH,NEW HAMPSHIRE +146,bwyxl-name,fdq-firstname, 13160,ngn-city,1954/07/05,nkco-street, 120,DE,DELAWARE +147,lkkwb-name,yqh-firstname, 19580,pwn-city,1954/10/16,rgdl-street, 185,MN,MINNESOTA +148,uokzd-name,aco-firstname, 13940,wyf-city,1966/02/07,lbhd-street, 23,NH,NEW HAMPSHIRE +149,tdmol-name,hkb-firstname, 11960,wbi-city,1970/06/03,wboh-street, 59,ND,NORTH DAKOTA +150,erulk-name,xcd-firstname, 11420,kzt-city,1990/02/07,bmcb-street, 160,DC,DISTRICT OF COLUMBIA +151,atrip-name,mlq-firstname, 14440,agk-city,1986/11/08,qhdv-street, 29,IN,INDIANA +152,atiir-name,brc-firstname, 11380,sas-city,1958/10/20,dwyv-street, 100,AR,ARKANSAS +153,cvygb-name,kdu-firstname, 17300,etl-city,1954/11/13,bdxo-street, 43,MA,MASSACHUSETTS +154,jeyeq-name,yjl-firstname, 12740,jgr-city,1978/06/21,aavd-street, 61,NY,NEW YORK +155,wojgm-name,xdk-firstname, 13340,meq-city,1982/10/20,ttix-street, 61,MT,MONTANA +156,oktge-name,taf-firstname, 11200,ibx-city,1990/09/05,clbk-street, 70,UT,UTAH +157,cdrrm-name,dmu-firstname, 11980,bqa-city,1962/06/18,owlk-street, 26,NY,NEW YORK +158,jyqvl-name,rht-firstname, 11120,qrk-city,1982/04/20,qbrn-street, 55,WY,WYOMING +159,spfxh-name,oqv-firstname, 14740,gyh-city,1970/07/08,oiin-street, 59,NC,NORTH CAROLINA +160,sczwy-name,mhg-firstname, 17860,izz-city,1970/08/25,xehg-street, 2,NJ,NEW JERSEY +161,lklcm-name,rcy-firstname, 11960,ycf-city,1982/07/04,path-street, 179,NJ,NEW JERSEY +162,jcluy-name,tlk-firstname, 10380,lsi-city,1970/03/17,ugqr-street, 138,NJ,NEW JERSEY +163,qqdvp-name,hsh-firstname, 18240,bqf-city,1982/01/01,nupe-street, 153,LA,LOUISIANA +164,rxlox-name,uoi-firstname, 15600,uvd-city,1954/04/03,gjmv-street, 197,MA,MASSACHUSETTS +165,kjypq-name,wgt-firstname, 14060,yrs-city,1978/06/09,ijks-street, 144,CO,COLORADO +166,zegdj-name,fpi-firstname, 13380,znp-city,1978/04/26,pdlh-street, 187,KY,KENTUCKY +167,ihkkq-name,gtk-firstname, 15740,qbg-city,1970/06/18,odsg-street, 95,CO,COLORADO +168,yhnuk-name,uhh-firstname, 16720,hoo-city,1978/06/20,vrcy-street, 186,KS,KANSAS +169,ftpvt-name,ufk-firstname, 13600,wat-city,1954/10/16,nxax-street, 112,OR,OREGON +170,xoiyz-name,xqq-firstname, 14560,kea-city,1986/08/10,bivl-street, 177,MN,MINNESOTA +171,wfeuq-name,qec-firstname, 16540,obq-city,1950/11/17,keyf-street, 108,UT,UTAH +172,pfrmg-name,tyi-firstname, 15360,tjx-city,1979/01/30,lyhr-street, 78,NE,NEBRASKA +173,najqw-name,ldk-firstname, 10220,bci-city,1982/04/01,qxuf-street, 84,MS,MISSISSIPPI +174,qbqrg-name,zyo-firstname, 13420,cdh-city,1958/06/13,gqst-street, 167,WY,WYOMING +175,lnaxv-name,zwt-firstname, 14740,lok-city,1962/10/06,mmdu-street, 149,KY,KENTUCKY +176,tpgpm-name,qie-firstname, 14960,opy-city,1958/07/14,uxfv-street, 158,SC,SOUTH CAROLINA +177,nltvw-name,ahc-firstname, 19520,uxf-city,1958/03/16,fwsy-street, 131,CA,CALIFORNIA +178,ujfpc-name,cwd-firstname, 13800,gki-city,1974/10/10,sgiq-street, 12,FL,FLORIDA +179,pehcm-name,mah-firstname, 15940,azs-city,1970/05/07,hvvk-street, 9,PR,PUERTO RICO +180,phlqr-name,qog-firstname, 12160,qvt-city,1966/09/11,isol-street, 155,AZ,ARIZONA +181,bxerk-name,kxv-firstname, 14180,sek-city,1982/02/18,ctwu-street, 84,CA,CALIFORNIA +182,nmlqw-name,oyf-firstname, 12640,tmv-city,1962/02/26,eqss-street, 141,NE,NEBRASKA +183,kobcl-name,pht-firstname, 15820,nky-city,1978/05/14,vfnd-street, 176,PR,PUERTO RICO +184,lvbqi-name,juh-firstname, 12780,rst-city,1958/12/18,wwko-street, 22,MP,NORTHERN MARIANA ISLANDS +185,yqqmt-name,zrg-firstname, 12780,hxs-city,1954/08/12,mdxh-street, 190,MS,MISSISSIPPI +186,osbyt-name,qtk-firstname, 14900,ltd-city,1990/06/21,quqn-street, 59,MO,MISSOURI +187,vibab-name,vgy-firstname, 19600,jxa-city,1958/09/20,czps-street, 137,AR,ARKANSAS +188,vrnml-name,qmd-firstname, 15860,mxe-city,1966/07/23,ybfc-street, 148,DE,DELAWARE +189,thimt-name,ige-firstname, 12900,dqn-city,1966/05/07,bccw-street, 187,OK,OKLAHOMA +190,jdzou-name,qnd-firstname, 17600,fzi-city,1958/06/12,ewtx-street, 174,IN,INDIANA +191,bsvvw-name,hfa-firstname, 14180,kmn-city,1974/09/19,zvdw-street, 13,UT,UTAH +192,iwbao-name,qur-firstname, 19500,jlk-city,1982/08/08,kllj-street, 113,WA,WASHINGTON +193,xpxla-name,yzv-firstname, 19020,eze-city,1954/04/22,taku-street, 105,AS,AMERICAN SAMOA +194,gqugh-name,sdy-firstname, 14360,pwi-city,1974/03/11,qybh-street, 95,KY,KENTUCKY +195,bueoc-name,sfx-firstname, 10560,xhn-city,1970/08/29,zfin-street, 48,NC,NORTH CAROLINA +196,fyrln-name,fay-firstname, 10820,qtd-city,1974/11/04,yrtc-street, 120,MH,MARSHALL ISLANDS +197,zuhli-name,qwr-firstname, 19800,nqp-city,1970/01/21,mdew-street, 8,VA,VIRGINIA +198,rwplk-name,jkr-firstname, 18080,khf-city,1978/02/28,ihkv-street, 134,MT,MONTANA +199,ssbzy-name,azn-firstname, 11440,ire-city,1954/11/16,sjou-street, 55,CO,COLORADO +200,zbhue-name,ces-firstname, 19840,ybc-city,1974/07/17,ktsw-street, 94,ND,NORTH DAKOTA +201,tcpnt-name,tgk-firstname, 19580,nox-city,1990/02/24,rmst-street, 59,MS,MISSISSIPPI +202,avrpe-name,aaz-firstname, 14000,anm-city,1950/09/02,ddjz-street, 197,FL,FLORIDA +203,qemau-name,lbl-firstname, 15620,jkx-city,1962/07/23,kxdn-street, 38,PA,PENNSYLVANIA +204,xzeqe-name,bjx-firstname, 12960,qiv-city,1958/09/07,yohx-street, 22,VA,VIRGINIA +205,ufbqh-name,dcm-firstname, 17720,tch-city,1978/10/16,sqis-street, 119,NM,NEW MEXICO +206,cfwje-name,kng-firstname, 15980,hmf-city,1974/09/23,timl-street, 105,NV,NEVADA +207,dpswi-name,lzu-firstname, 11020,mby-city,1962/10/14,stnj-street, 143,UT,UTAH +208,padrh-name,yvj-firstname, 17680,pqc-city,1986/11/28,xmxq-street, 81,GU,GUAM +209,blcun-name,erh-firstname, 16200,sgc-city,1950/10/10,sqkp-street, 29,PA,PENNSYLVANIA +210,jxuox-name,ztl-firstname, 13140,hox-city,1962/08/12,vxgj-street, 83,KS,KANSAS +211,bcfua-name,urk-firstname, 11540,mhn-city,1982/10/09,poor-street, 21,VI,VIRGIN ISLANDS +212,nmccn-name,nlv-firstname, 11780,dec-city,1974/07/05,txyt-street, 125,FL,FLORIDA +213,cwcfl-name,nye-firstname, 10620,ciu-city,1974/06/14,dumh-street, 124,TX,TEXAS +214,bewhj-name,mcq-firstname, 16040,vir-city,1951/01/31,uhse-street, 78,MA,MASSACHUSETTS +215,owbls-name,mcq-firstname, 12940,zjk-city,1962/08/21,plgy-street, 185,NJ,NEW JERSEY +216,tjwtx-name,uur-firstname, 13400,jqa-city,1962/05/15,hhzi-street, 134,KS,KANSAS +217,aynmw-name,obp-firstname, 13820,hbu-city,1974/12/09,lfeb-street, 28,NV,NEVADA +218,aivbc-name,fkc-firstname, 14980,mew-city,1958/05/19,rxqg-street, 41,AK,ALASKA +219,nkeha-name,ddi-firstname, 17680,wzu-city,1986/03/04,xtik-street, 11,RI,RHODE ISLAND +220,hyyqm-name,vac-firstname, 17600,gph-city,1954/04/28,rjxi-street, 22,NY,NEW YORK +221,sifar-name,yth-firstname, 12840,kbe-city,1982/11/18,mnje-street, 5,NY,NEW YORK +222,bxlyk-name,pla-firstname, 15740,zpb-city,1990/12/07,viys-street, 171,HI,HAWAII +223,ifcwk-name,yfu-firstname, 10000,itv-city,1982/06/16,iuya-street, 77,SD,SOUTH DAKOTA +224,fherw-name,acw-firstname, 13000,dxg-city,1970/09/09,zscv-street, 45,VT,VERMONT +225,cvbfh-name,tbs-firstname, 13160,znt-city,1958/07/20,exfl-street, 171,LA,LOUISIANA +226,sxpuq-name,qdu-firstname, 13000,lhm-city,1971/01/08,yooq-street, 80,VT,VERMONT +227,xawor-name,glz-firstname, 18160,dxx-city,1954/12/08,fjnf-street, 130,FM,FEDERATED STATES OF MICRONESIA +228,dwpda-name,dtg-firstname, 15380,zyz-city,1974/04/21,gozg-street, 96,MT,MONTANA +229,airyv-name,oue-firstname, 16900,gbm-city,1986/07/14,xfte-street, 45,MP,NORTHERN MARIANA ISLANDS +230,omfog-name,zhv-firstname, 17020,lep-city,1970/03/21,trww-street, 128,LA,LOUISIANA +231,ddgah-name,ost-firstname, 13580,ojl-city,1958/03/07,gnln-street, 155,KY,KENTUCKY +232,feggq-name,cro-firstname, 18780,wtj-city,1966/10/01,jesi-street, 63,NM,NEW MEXICO +233,ahxvq-name,nes-firstname, 11660,niu-city,1950/06/06,upyk-street, 185,WA,WASHINGTON +234,gjlqf-name,mvv-firstname, 19620,roc-city,1974/05/01,tsqu-street, 19,MI,MICHIGAN +235,xbcip-name,vyn-firstname, 10560,nru-city,1986/12/06,qxfi-street, 114,WI,WISCONSIN +236,cdwuj-name,sks-firstname, 12560,typ-city,1954/01/31,fkwb-street, 128,DC,DISTRICT OF COLUMBIA +237,bsekx-name,wbw-firstname, 14280,twm-city,1962/09/11,bxui-street, 174,NM,NEW MEXICO +238,foppd-name,zlw-firstname, 13580,hmg-city,1974/04/29,yiwk-street, 68,MN,MINNESOTA +239,brtej-name,cqi-firstname, 11000,elz-city,1982/10/16,uauh-street, 23,PA,PENNSYLVANIA +240,cpklp-name,tps-firstname, 11440,nsm-city,1950/10/28,cmjv-street, 139,PR,PUERTO RICO +241,opbzn-name,bxz-firstname, 12860,jnq-city,1966/05/08,nkuq-street, 35,MP,NORTHERN MARIANA ISLANDS +242,kxkmx-name,ziy-firstname, 17460,wqq-city,1974/11/05,gnha-street, 192,WV,WEST VIRGINIA +243,lxpbu-name,jph-firstname, 19500,fpa-city,1954/10/01,gdls-street, 163,MI,MICHIGAN +244,erhwd-name,wvu-firstname, 11880,iza-city,1962/08/03,ucsh-street, 75,undefined,undefined +245,tjuxy-name,jzf-firstname, 10580,nyq-city,1970/04/30,gbes-street, 189,AR,ARKANSAS +246,piocq-name,skz-firstname, 14600,xuq-city,1978/07/12,inae-street, 27,ME,MAINE +247,bqjty-name,ybj-firstname, 13040,jqu-city,1954/11/30,ugcn-street, 159,AL,ALABAMA +248,xexrx-name,fpu-firstname, 19720,ckc-city,1974/06/30,zpez-street, 46,IN,INDIANA +249,auzgu-name,dam-firstname, 18460,mih-city,1962/07/28,augp-street, 112,MD,MARYLAND +250,xupoe-name,fdb-firstname, 13440,llr-city,1986/10/01,forq-street, 185,IL,ILLINOIS +251,sgely-name,pzz-firstname, 15920,jya-city,1950/02/02,kypg-street, 147,DC,DISTRICT OF COLUMBIA +252,onini-name,zts-firstname, 18060,avs-city,1974/12/02,kxjn-street, 85,TX,TEXAS +253,tyflk-name,htl-firstname, 17560,bhd-city,1962/06/06,xquf-street, 126,IN,INDIANA +254,chbez-name,zkj-firstname, 17000,goh-city,1974/03/02,rkui-street, 13,AR,ARKANSAS +255,zmmhg-name,rqb-firstname, 11340,egt-city,1970/02/06,pwjj-street, 6,MH,MARSHALL ISLANDS +256,gutfo-name,vki-firstname, 18860,xdv-city,1986/07/11,iwkf-street, 14,DC,DISTRICT OF COLUMBIA +257,eogwr-name,hnt-firstname, 19840,nht-city,1990/05/02,kljr-street, 18,VT,VERMONT +258,ibupi-name,ygc-firstname, 18580,tvk-city,1978/08/20,xphm-street, 123,CO,COLORADO +259,wqpaq-name,uwc-firstname, 10780,ygl-city,1958/03/20,ncta-street, 87,GU,GUAM +260,swcms-name,ljb-firstname, 13560,pvt-city,1954/12/13,ovsf-street, 176,MD,MARYLAND +261,xwxmt-name,qpq-firstname, 13380,fzc-city,1978/10/14,ivwb-street, 17,AZ,ARIZONA +262,drxej-name,msd-firstname, 12720,ure-city,1986/02/15,nrqa-street, 30,MT,MONTANA +263,scgpq-name,wgg-firstname, 18740,xtx-city,1990/05/09,vxwl-street, 29,AK,ALASKA +264,qchcm-name,fbc-firstname, 14480,ymf-city,1978/03/21,cfqc-street, 63,ND,NORTH DAKOTA +265,cbpxm-name,clb-firstname, 16900,etx-city,1974/03/29,uzyw-street, 175,OK,OKLAHOMA +266,clksy-name,mls-firstname, 13560,qhs-city,1958/04/07,trwx-street, 47,MO,MISSOURI +267,qwagv-name,hil-firstname, 18240,atx-city,1954/01/30,glaf-street, 57,MT,MONTANA +268,grkjm-name,qwy-firstname, 15900,jvu-city,1970/02/26,rhdn-street, 172,ME,MAINE +269,gkiis-name,xhp-firstname, 14440,bgh-city,1954/01/02,btrx-street, 53,FL,FLORIDA +270,nscdn-name,lfn-firstname, 19900,eph-city,1958/09/29,nqao-street, 171,KS,KANSAS +271,ulrrc-name,ncb-firstname, 10320,cao-city,1986/10/15,lkrm-street, 125,GA,GEORGIA +272,qxjcn-name,wpy-firstname, 11260,rew-city,1954/12/19,tldv-street, 115,CA,CALIFORNIA +273,thyyj-name,htd-firstname, 19100,tae-city,1982/03/04,wbqv-street, 174,PR,PUERTO RICO +274,uigiq-name,lmx-firstname, 19320,bkr-city,1950/04/07,foio-street, 104,OK,OKLAHOMA +275,iinbr-name,cfg-firstname, 12100,qwv-city,1986/01/12,ervo-street, 87,MN,MINNESOTA +276,kipbw-name,fda-firstname, 16300,mlu-city,1970/03/26,yjpd-street, 15,MN,MINNESOTA +277,cfrgd-name,dui-firstname, 11320,jax-city,1990/04/14,zaik-street, 157,ME,MAINE +278,duaza-name,xsx-firstname, 11120,kkg-city,1958/02/05,bply-street, 185,VI,VIRGIN ISLANDS +279,zzhqx-name,vlb-firstname, 18380,vyb-city,1950/12/11,iugj-street, 159,HI,HAWAII +280,kuryf-name,kib-firstname, 13140,tum-city,1954/05/06,clkw-street, 31,CO,COLORADO +281,daljt-name,ycr-firstname, 10840,ckw-city,1982/10/29,umof-street, 52,CO,COLORADO +282,xhssg-name,djc-firstname, 17840,gvj-city,1954/12/12,zgmw-street, 35,WV,WEST VIRGINIA +283,qqscv-name,eiu-firstname, 15260,daj-city,1986/01/30,qprc-street, 158,MH,MARSHALL ISLANDS +284,tehbb-name,czj-firstname, 14180,xnh-city,1958/03/06,lfji-street, 48,UT,UTAH +285,acdsd-name,yiu-firstname, 12220,buk-city,1958/02/15,ovcj-street, 70,ME,MAINE +286,derpl-name,buv-firstname, 16000,fha-city,1970/09/02,vfay-street, 57,WA,WASHINGTON +287,lobqd-name,qxn-firstname, 15060,ycp-city,1966/12/24,axxb-street, 38,PA,PENNSYLVANIA +288,owxja-name,okb-firstname, 15640,lad-city,1982/05/24,wnka-street, 101,MN,MINNESOTA +289,zohkw-name,ypo-firstname, 15460,wtu-city,1978/08/10,jhco-street, 21,VA,VIRGINIA +290,nmrar-name,aqm-firstname, 18360,lcn-city,1963/01/26,zxeo-street, 143,WV,WEST VIRGINIA +291,tndrh-name,ael-firstname, 16360,cyb-city,1958/04/10,cmlg-street, 78,ND,NORTH DAKOTA +292,kbpkv-name,yck-firstname, 19400,oka-city,1974/11/02,kpmc-street, 10,CO,COLORADO +293,uabcl-name,zms-firstname, 15940,tzb-city,1970/08/06,ezzs-street, 11,VT,VERMONT +294,lancp-name,zbk-firstname, 11560,vny-city,1978/07/21,tzys-street, 182,LA,LOUISIANA +295,wagzv-name,hcp-firstname, 13760,kik-city,1974/11/01,gpuw-street, 151,NH,NEW HAMPSHIRE +296,qrenr-name,egp-firstname, 16920,zwq-city,1966/03/19,fbwi-street, 102,FL,FLORIDA +297,amjep-name,mds-firstname, 16700,fvb-city,1978/05/07,peau-street, 167,WV,WEST VIRGINIA +298,dzppv-name,qav-firstname, 16680,wbc-city,1970/04/15,dzyp-street, 149,VI,VIRGIN ISLANDS +299,qrlwz-name,hvk-firstname, 14640,qyl-city,1954/03/07,gvsc-street, 32,NC,NORTH CAROLINA +300,lracx-name,dmp-firstname, 13800,slo-city,1970/09/21,jspb-street, 106,WA,WASHINGTON +301,jhlao-name,txt-firstname, 12020,jam-city,1962/06/26,bcky-street, 19,WV,WEST VIRGINIA +302,aocxq-name,mzv-firstname, 17140,dgx-city,1954/08/24,maoi-street, 1,HI,HAWAII +303,dvdbu-name,fdf-firstname, 10980,goa-city,1982/04/10,onik-street, 109,NM,NEW MEXICO +304,cqapx-name,skq-firstname, 11080,akw-city,1958/09/02,xakx-street, 14,CA,CALIFORNIA +305,crmlf-name,djf-firstname, 16100,mle-city,1974/10/10,udrl-street, 96,VT,VERMONT +306,iujcn-name,tat-firstname, 17660,jnf-city,1966/03/31,inoh-street, 104,TX,TEXAS +307,aetvh-name,spn-firstname, 14960,wxf-city,1954/06/30,mmxe-street, 78,PA,PENNSYLVANIA +308,ibfdt-name,sfu-firstname, 11120,kqf-city,1954/06/22,xbwg-street, 132,CA,CALIFORNIA +309,ccatv-name,aeb-firstname, 10940,qoi-city,1982/08/11,bsrg-street, 29,UT,UTAH +310,udrjw-name,agc-firstname, 17100,aep-city,1974/04/23,bsju-street, 59,PR,PUERTO RICO +311,crjcx-name,nbb-firstname, 14820,xtp-city,1958/11/08,fajh-street, 95,WY,WYOMING +312,qbcrw-name,mef-firstname, 14220,mwa-city,1982/11/13,keyy-street, 97,NE,NEBRASKA +313,wxvpi-name,dym-firstname, 10220,ncp-city,1954/09/28,qgrg-street, 25,undefined,undefined +314,bandc-name,hzf-firstname, 18700,eoh-city,1990/11/05,qphi-street, 177,IL,ILLINOIS +315,afatf-name,eii-firstname, 15480,lsm-city,1982/10/11,nzjq-street, 96,VI,VIRGIN ISLANDS +316,trxge-name,vhe-firstname, 18260,ccm-city,1958/03/09,ducm-street, 1,NH,NEW HAMPSHIRE +317,kdygt-name,tgc-firstname, 12500,bqo-city,1978/06/20,rqgx-street, 90,DC,DISTRICT OF COLUMBIA +318,angdt-name,lvt-firstname, 13960,tbr-city,1974/07/14,wfqj-street, 196,GU,GUAM +319,hqhqe-name,nxd-firstname, 14860,yhi-city,1954/04/23,wkpi-street, 23,MP,NORTHERN MARIANA ISLANDS +320,pwwep-name,qtw-firstname, 15160,utn-city,1958/06/08,sheh-street, 176,FL,FLORIDA +321,vkken-name,wrw-firstname, 16760,jhm-city,1974/12/13,czmf-street, 183,AR,ARKANSAS +322,zbbmz-name,ipe-firstname, 14340,mco-city,1970/04/25,ymou-street, 2,WY,WYOMING +323,eyzfr-name,xeb-firstname, 10440,qsa-city,1990/04/15,iukl-street, 135,AZ,ARIZONA +324,tfpxy-name,yzh-firstname, 14080,wbc-city,1970/02/12,ojfc-street, 150,MP,NORTHERN MARIANA ISLANDS +325,tphou-name,xac-firstname, 15940,ncy-city,1962/09/22,vcxl-street, 90,AL,ALABAMA +326,bfscx-name,yih-firstname, 18900,bxa-city,1962/10/12,qsww-street, 137,VT,VERMONT +327,ussbw-name,gbb-firstname, 15600,wtg-city,1978/07/29,vqcq-street, 10,KY,KENTUCKY +328,zkoqj-name,mqn-firstname, 13440,lux-city,1970/05/01,bokx-street, 106,VA,VIRGINIA +329,zbqew-name,dbw-firstname, 14520,hbi-city,1971/01/20,syea-street, 192,CT,CONNECTICUT +330,vmfuy-name,qge-firstname, 16660,vnk-city,1982/08/29,tlxy-street, 166,PW,PALAU +331,wajgr-name,mnx-firstname, 17500,wiv-city,1954/07/10,ylug-street, 21,OR,OREGON +332,lzcry-name,tzk-firstname, 18980,icz-city,1966/06/04,ldvu-street, 25,GU,GUAM +333,rodpv-name,rix-firstname, 16540,tpf-city,1986/02/03,leur-street, 169,SD,SOUTH DAKOTA +334,pxmhq-name,tyd-firstname, 11200,okj-city,1978/12/30,tauk-street, 94,ND,NORTH DAKOTA +335,kansg-name,qzs-firstname, 18020,yhj-city,1982/08/27,ehie-street, 140,AR,ARKANSAS +336,kpqff-name,bqs-firstname, 13780,tnp-city,1958/08/07,euhw-street, 182,SD,SOUTH DAKOTA +337,akvdd-name,bxi-firstname, 15620,rbk-city,1962/03/16,fzdy-street, 125,WA,WASHINGTON +338,nbazl-name,ikv-firstname, 13160,wci-city,1966/08/22,smzb-street, 136,SD,SOUTH DAKOTA +339,zrowi-name,udo-firstname, 16460,hbe-city,1962/07/07,xgyd-street, 170,AZ,ARIZONA +340,jfzjd-name,atn-firstname, 13040,yef-city,1974/09/26,dyjj-street, 127,WI,WISCONSIN +341,prsbo-name,ibh-firstname, 19520,qro-city,1986/01/10,pemg-street, 183,SC,SOUTH CAROLINA +342,dwfdn-name,kci-firstname, 14640,fze-city,1970/07/24,xxdz-street, 102,IA,IOWA +343,cnupm-name,rjl-firstname, 14660,ewk-city,1966/07/02,wzjr-street, 107,PW,PALAU +344,wbpor-name,fuf-firstname, 11300,xne-city,1978/02/14,tyzx-street, 2,AL,ALABAMA +345,tjqky-name,mjf-firstname, 12560,qlo-city,1982/10/21,hzos-street, 121,NE,NEBRASKA +346,yxqsn-name,ofx-firstname, 18680,ute-city,1974/02/27,wqka-street, 111,FL,FLORIDA +347,wjwmv-name,hra-firstname, 14520,mvj-city,1954/10/03,chmz-street, 70,SD,SOUTH DAKOTA +348,aipnn-name,paa-firstname, 14400,gyq-city,1970/04/27,tbpq-street, 84,undefined,undefined +349,wplyl-name,zvh-firstname, 15820,vhw-city,1967/01/12,eesj-street, 110,undefined,undefined +350,syoip-name,lbd-firstname, 13860,wfo-city,1966/03/17,lsvf-street, 47,VI,VIRGIN ISLANDS +351,lvhdk-name,pzl-firstname, 14320,bab-city,1950/05/01,wktz-street, 134,AR,ARKANSAS +352,jodya-name,apd-firstname, 14520,nfr-city,1986/12/14,jgzs-street, 193,KY,KENTUCKY +353,ikunj-name,syx-firstname, 17320,cqd-city,1982/08/19,xwzj-street, 53,IN,INDIANA +354,bnhud-name,uvd-firstname, 15360,ynw-city,1971/01/25,gens-street, 107,MT,MONTANA +355,prkrf-name,ivm-firstname, 19220,kie-city,1966/11/14,boem-street, 59,HI,HAWAII +356,ucywi-name,sjl-firstname, 11480,ish-city,1982/11/13,szck-street, 148,NE,NEBRASKA +357,sgftd-name,ajp-firstname, 11760,jco-city,1958/03/05,djek-street, 196,UT,UTAH +358,kohdq-name,phr-firstname, 13120,kpb-city,1978/07/26,csom-street, 17,PR,PUERTO RICO +359,fgjoa-name,srp-firstname, 11480,onn-city,1990/05/26,duvo-street, 127,MS,MISSISSIPPI +360,xgiyw-name,rcu-firstname, 15140,mti-city,1958/09/26,lego-street, 125,MA,MASSACHUSETTS +361,kgcui-name,grq-firstname, 12640,tjn-city,1986/02/08,ntoq-street, 6,CT,CONNECTICUT +362,ljyxw-name,lho-firstname, 19100,hmd-city,1962/03/09,mqus-street, 12,CT,CONNECTICUT +363,vnvwy-name,vmv-firstname, 14360,cpu-city,1966/04/20,cfts-street, 16,MI,MICHIGAN +364,gedwu-name,ocl-firstname, 11760,vaf-city,1970/04/22,exot-street, 121,MT,MONTANA +365,enknw-name,vjk-firstname, 11100,bge-city,1954/07/06,inhb-street, 95,AS,AMERICAN SAMOA +366,qmpom-name,tmp-firstname, 12260,ymn-city,1986/07/02,psdj-street, 71,MI,MICHIGAN +367,jrbwj-name,imr-firstname, 11080,qsx-city,1966/11/08,msys-street, 80,ID,IDAHO +368,dzjpp-name,cwl-firstname, 15720,ipm-city,1958/07/13,acsb-street, 87,IL,ILLINOIS +369,qdwdn-name,dtc-firstname, 12740,qab-city,1986/11/29,besk-street, 80,DC,DISTRICT OF COLUMBIA +370,gbeyp-name,lzl-firstname, 19740,ljz-city,1974/10/01,zwga-street, 52,RI,RHODE ISLAND +371,kuwva-name,noq-firstname, 17500,xia-city,1950/03/18,omzn-street, 164,KS,KANSAS +372,jjhsm-name,cdc-firstname, 13020,xli-city,1986/06/10,nups-street, 38,VA,VIRGINIA +373,xyupl-name,fyd-firstname, 16100,fqd-city,1971/01/05,icjo-street, 28,NE,NEBRASKA +374,ueipj-name,meb-firstname, 19880,nsk-city,1974/08/15,aqwr-street, 14,LA,LOUISIANA +375,vdmif-name,fat-firstname, 19120,dud-city,1974/07/01,krgw-street, 178,MS,MISSISSIPPI +376,oxgdf-name,apn-firstname, 11400,dji-city,1962/04/02,ttnt-street, 13,HI,HAWAII +377,xgxyc-name,jrn-firstname, 19400,bfg-city,1950/12/30,jzgy-street, 43,WY,WYOMING +378,bczfk-name,bfu-firstname, 16920,qxp-city,1974/05/26,seja-street, 178,HI,HAWAII +379,pppqe-name,kwo-firstname, 15260,geb-city,1986/04/06,okvv-street, 11,MN,MINNESOTA +380,opzyy-name,mfk-firstname, 14960,nlo-city,1962/03/03,dezo-street, 106,AS,AMERICAN SAMOA +381,xmnsm-name,ckj-firstname, 10400,fnr-city,1950/10/21,uhme-street, 154,NC,NORTH CAROLINA +382,moqtn-name,zgw-firstname, 12920,pqk-city,1950/10/28,suvi-street, 102,WY,WYOMING +383,byapu-name,pix-firstname, 10900,gik-city,1982/09/04,ntiq-street, 45,VI,VIRGIN ISLANDS +384,zuhdb-name,gbj-firstname, 18760,vkk-city,1978/06/17,vnem-street, 62,TX,TEXAS +385,gkjpo-name,qwq-firstname, 12380,ame-city,1982/12/01,ndvp-street, 94,VA,VIRGINIA +386,yefwf-name,aev-firstname, 13580,mor-city,1962/07/09,ldcg-street, 91,AL,ALABAMA +387,twgsp-name,mwx-firstname, 15840,bbp-city,1970/05/12,hecl-street, 137,WY,WYOMING +388,zatdb-name,tes-firstname, 17080,yga-city,1954/11/05,dfwu-street, 58,undefined,undefined +389,qxdyx-name,tum-firstname, 18100,mqw-city,1958/10/21,gndl-street, 156,FL,FLORIDA +390,ncpki-name,wbm-firstname, 13860,cuo-city,1970/07/02,yqpa-street, 30,NE,NEBRASKA +391,kyzsl-name,djj-firstname, 17400,zzd-city,1978/02/11,mvkp-street, 4,ME,MAINE +392,jtksy-name,ayp-firstname, 10760,gui-city,1982/12/17,wohf-street, 175,DC,DISTRICT OF COLUMBIA +393,grzby-name,oyz-firstname, 15140,bmz-city,1974/07/10,rrui-street, 60,NC,NORTH CAROLINA +394,sgaut-name,xzd-firstname, 15260,dnb-city,1978/10/01,hcii-street, 169,RI,RHODE ISLAND +395,ozkaq-name,brx-firstname, 11400,guw-city,1962/02/08,pswz-street, 194,NH,NEW HAMPSHIRE +396,uqivg-name,map-firstname, 10880,lgr-city,1990/07/14,lxmi-street, 143,MN,MINNESOTA +397,soqkg-name,jws-firstname, 17200,dss-city,1970/12/05,ppht-street, 187,UT,UTAH +398,pdqdm-name,erh-firstname, 11860,obj-city,1986/04/03,aova-street, 121,CT,CONNECTICUT +399,ogqrv-name,uyf-firstname, 16400,abs-city,1987/01/06,xwue-street, 114,AZ,ARIZONA +400,ukcxw-name,ltd-firstname, 11760,bwi-city,1986/09/01,ddjt-street, 199,AR,ARKANSAS +401,djcgr-name,tet-firstname, 19380,ure-city,1962/09/03,alzp-street, 169,SC,SOUTH CAROLINA +402,ibbvs-name,cyv-firstname, 16060,gdh-city,1982/04/24,qrdz-street, 116,NV,NEVADA +403,blmke-name,jtq-firstname, 17260,tls-city,1970/05/28,eylx-street, 171,NV,NEVADA +404,qpatk-name,mtt-firstname, 13400,nzv-city,1970/03/25,exby-street, 93,WY,WYOMING +405,mbngf-name,pqp-firstname, 10720,ann-city,1966/09/01,rkpu-street, 146,CT,CONNECTICUT +406,rcydx-name,usf-firstname, 12880,jou-city,1982/02/20,gtqw-street, 186,MO,MISSOURI +407,srszq-name,dtb-firstname, 14340,yrs-city,1978/03/16,wtan-street, 89,MN,MINNESOTA +408,ezlfr-name,ebd-firstname, 19420,ctw-city,1986/03/22,zjyv-street, 44,VT,VERMONT +409,bhotj-name,hzt-firstname, 16480,rbt-city,1978/06/11,mbsm-street, 157,WV,WEST VIRGINIA +410,udzwf-name,kfd-firstname, 13440,maj-city,1986/06/06,kerz-street, 180,AZ,ARIZONA +411,yflfv-name,zdl-firstname, 13300,zix-city,1982/05/20,ozcp-street, 153,MN,MINNESOTA +412,wlyyq-name,kdz-firstname, 11400,ygb-city,1966/11/07,zekv-street, 111,AS,AMERICAN SAMOA +413,wonzh-name,eeb-firstname, 19920,qnt-city,1982/09/20,fxob-street, 95,LA,LOUISIANA +414,eiwzh-name,hpm-firstname, 15860,bfo-city,1967/01/30,tkpg-street, 182,VA,VIRGINIA +415,mbqgk-name,jsm-firstname, 15040,dai-city,1954/06/12,jqdh-street, 65,NE,NEBRASKA +416,jhigj-name,qyn-firstname, 18360,ntm-city,1974/05/15,eghd-street, 188,ME,MAINE +417,llytl-name,jqe-firstname, 11280,kkp-city,1974/04/03,oudg-street, 69,DE,DELAWARE +418,urhgl-name,iji-firstname, 13760,lhf-city,1962/12/02,oywx-street, 6,SC,SOUTH CAROLINA +419,uflrm-name,hef-firstname, 15040,usl-city,1974/11/06,qvgi-street, 103,OK,OKLAHOMA +420,gbfui-name,goz-firstname, 14940,edu-city,1986/02/25,gkqy-street, 26,KS,KANSAS +421,nysbp-name,fro-firstname, 18420,wqt-city,1958/12/19,vpoc-street, 89,GA,GEORGIA +422,fmrwo-name,wlf-firstname, 17820,qwb-city,1962/05/03,stcz-street, 22,CA,CALIFORNIA +423,racwd-name,kqr-firstname, 10180,xdr-city,1974/04/09,fqxz-street, 15,MO,MISSOURI +424,jpopz-name,krm-firstname, 13420,fjx-city,1970/10/29,kyph-street, 54,NJ,NEW JERSEY +425,fwdat-name,ppn-firstname, 15660,zqh-city,1986/06/24,zgfe-street, 61,SC,SOUTH CAROLINA +426,orznz-name,hyy-firstname, 12020,lju-city,1982/07/01,xisy-street, 125,GU,GUAM +427,spzxo-name,mpv-firstname, 13220,cbq-city,1962/05/09,qlqx-street, 53,OK,OKLAHOMA +428,qxdra-name,ifp-firstname, 10480,nvu-city,1958/08/15,egsq-street, 133,MH,MARSHALL ISLANDS +429,yhelu-name,jsc-firstname, 17200,clp-city,1954/12/01,vahx-street, 3,NV,NEVADA +430,umvfv-name,mbe-firstname, 13600,knp-city,1954/07/16,oldf-street, 188,PA,PENNSYLVANIA +431,mwcfe-name,xxi-firstname, 15080,chq-city,1974/12/22,kpsj-street, 163,NV,NEVADA +432,wvkrp-name,dtr-firstname, 19840,pqv-city,1986/07/07,jnzd-street, 119,HI,HAWAII +433,vqfja-name,kep-firstname, 19380,ydo-city,1966/11/11,gsfd-street, 12,AR,ARKANSAS +434,ikbjr-name,ipd-firstname, 17920,uld-city,1990/03/03,tmbc-street, 49,KS,KANSAS +435,hyjrs-name,jqp-firstname, 18820,vvm-city,1970/05/07,txye-street, 152,AR,ARKANSAS +436,tuewv-name,lkd-firstname, 17060,slo-city,1970/07/06,htdm-street, 197,OK,OKLAHOMA +437,muyws-name,iql-firstname, 19540,cwf-city,1962/04/27,knsx-street, 167,LA,LOUISIANA +438,tgsga-name,lww-firstname, 17780,goh-city,1982/12/24,lzcl-street, 136,SD,SOUTH DAKOTA +439,agsyj-name,fve-firstname, 19260,wgi-city,1970/03/07,aone-street, 48,WV,WEST VIRGINIA +440,yrgqp-name,tni-firstname, 15820,mzp-city,1986/04/19,femc-street, 29,PR,PUERTO RICO +441,ltjmw-name,cps-firstname, 13060,aeo-city,1954/07/17,bewv-street, 182,OK,OKLAHOMA +442,gjkpl-name,tre-firstname, 16340,ndn-city,1962/08/27,ocld-street, 16,WV,WEST VIRGINIA +443,ryvgz-name,bhe-firstname, 14960,lcg-city,1978/08/09,bxwa-street, 19,NM,NEW MEXICO +444,zgwwi-name,umb-firstname, 11840,llj-city,1958/07/06,uiww-street, 115,MP,NORTHERN MARIANA ISLANDS +445,qaczz-name,qng-firstname, 10900,umr-city,1970/01/17,mlsy-street, 6,VA,VIRGINIA +446,xcruf-name,vbp-firstname, 17840,vzl-city,1982/06/17,oatg-street, 110,MN,MINNESOTA +447,egqdv-name,boy-firstname, 18360,gfw-city,1958/10/07,qaoh-street, 104,AS,AMERICAN SAMOA +448,digvt-name,fid-firstname, 11180,iyw-city,1958/03/13,pooo-street, 119,NV,NEVADA +449,gxavv-name,bal-firstname, 17020,cra-city,1966/02/01,nchf-street, 122,CO,COLORADO +450,tiwoz-name,vwo-firstname, 19340,oja-city,1982/07/16,bnsy-street, 149,MA,MASSACHUSETTS +451,jemtu-name,gnk-firstname, 11180,gbb-city,1954/03/01,pmbh-street, 87,NC,NORTH CAROLINA +452,rhdjl-name,qaf-firstname, 16500,nqr-city,1974/03/28,vmrd-street, 75,DE,DELAWARE +453,qdkbn-name,cpl-firstname, 17460,ugb-city,1987/01/29,cwoa-street, 184,ME,MAINE +454,tuhon-name,bbg-firstname, 10500,cer-city,1958/03/02,ttst-street, 184,WA,WASHINGTON +455,wluxg-name,xpv-firstname, 18240,axq-city,1958/07/28,useg-street, 140,NY,NEW YORK +456,ojkgh-name,tzq-firstname, 16000,guv-city,1990/11/25,hagy-street, 198,MN,MINNESOTA +457,arxsx-name,ana-firstname, 18620,oxx-city,1986/04/22,hkdb-street, 28,SD,SOUTH DAKOTA +458,ujpiw-name,bty-firstname, 14000,kiy-city,1978/08/10,bmgt-street, 176,PW,PALAU +459,sufuk-name,izq-firstname, 17740,kfy-city,1986/10/29,xxcz-street, 26,MD,MARYLAND +460,nedie-name,ajz-firstname, 11820,fnf-city,1970/04/08,ccnd-street, 108,MT,MONTANA +461,vbfwv-name,anp-firstname, 19880,qag-city,1962/11/17,tbcc-street, 23,NJ,NEW JERSEY +462,bmrzz-name,yfe-firstname, 11300,rgi-city,1970/10/01,xbjp-street, 26,FM,FEDERATED STATES OF MICRONESIA +463,jewvw-name,ymh-firstname, 13100,kcv-city,1982/03/01,cjbt-street, 4,KY,KENTUCKY +464,kxxpm-name,has-firstname, 18500,hlp-city,1954/04/03,qsmq-street, 77,MD,MARYLAND +465,qkjxa-name,gdq-firstname, 12240,qtz-city,1954/11/25,mevz-street, 12,NM,NEW MEXICO +466,vmdwq-name,vjm-firstname, 19980,rmz-city,1986/09/09,ifxg-street, 139,NC,NORTH CAROLINA +467,ssgil-name,lkd-firstname, 14220,ndd-city,1963/01/19,rjln-street, 195,MD,MARYLAND +468,szbhe-name,pwi-firstname, 10100,iij-city,1966/04/09,mojp-street, 177,AK,ALASKA +469,eswrp-name,lts-firstname, 10080,cuk-city,1966/03/29,plor-street, 139,VI,VIRGIN ISLANDS +470,tioeh-name,qgc-firstname, 11800,zre-city,1954/06/05,owaq-street, 98,LA,LOUISIANA +471,fkzpf-name,bse-firstname, 19040,cor-city,1962/06/21,aamy-street, 53,NY,NEW YORK +472,kczhq-name,hde-firstname, 16380,siz-city,1986/12/15,rawc-street, 127,ND,NORTH DAKOTA +473,gpwqf-name,pae-firstname, 12820,rga-city,1958/12/19,djsk-street, 131,WY,WYOMING +474,yvgmq-name,hzp-firstname, 19020,ioc-city,1966/03/22,zdbt-street, 106,FM,FEDERATED STATES OF MICRONESIA +475,abjqk-name,pdo-firstname, 13040,vnj-city,1962/09/08,kvwv-street, 145,OR,OREGON +476,adppx-name,gmz-firstname, 16560,pah-city,1962/03/14,ynqs-street, 107,WY,WYOMING +477,lxcrs-name,arg-firstname, 12160,med-city,1990/03/14,wlag-street, 141,MT,MONTANA +478,mrkfp-name,jbm-firstname, 19760,dhu-city,1970/06/12,idan-street, 145,UT,UTAH +479,zmkad-name,vns-firstname, 10080,aoe-city,1955/01/25,qgbd-street, 174,FL,FLORIDA +480,vftgh-name,nxs-firstname, 11580,igp-city,1954/05/17,fief-street, 183,MT,MONTANA +481,bnafy-name,geg-firstname, 11160,sfp-city,1974/04/26,tpnq-street, 194,DE,DELAWARE +482,mwqpn-name,lbw-firstname, 17660,oot-city,1974/04/09,qxgk-street, 18,AK,ALASKA +483,cijcf-name,uvd-firstname, 17940,ioy-city,1982/02/07,gfiz-street, 147,MN,MINNESOTA +484,kwjhv-name,swd-firstname, 12400,pue-city,1970/12/10,sall-street, 104,MN,MINNESOTA +485,mfdfy-name,hsy-firstname, 18260,bzl-city,1982/09/08,hsyc-street, 76,RI,RHODE ISLAND +486,cdrmm-name,mxa-firstname, 11520,rie-city,1962/12/21,dxed-street, 112,LA,LOUISIANA +487,flyur-name,mzm-firstname, 12260,wyi-city,1978/07/07,xqoj-street, 156,WV,WEST VIRGINIA +488,hkwnf-name,obg-firstname, 14520,bib-city,1967/01/20,mvee-street, 115,undefined,undefined +489,kawax-name,pyn-firstname, 10520,zjh-city,1966/09/01,fsmz-street, 87,DE,DELAWARE +490,nnhzs-name,sfo-firstname, 19040,thn-city,1990/11/08,tzsb-street, 43,IN,INDIANA +491,xivec-name,gzo-firstname, 16820,aha-city,1978/10/19,iolt-street, 36,HI,HAWAII +492,hyiju-name,plw-firstname, 18620,zzu-city,1982/07/13,tydq-street, 112,NV,NEVADA +493,leylb-name,fcv-firstname, 15720,biw-city,1958/09/18,bnpf-street, 52,MT,MONTANA +494,pweci-name,hcu-firstname, 19020,fzb-city,1950/10/28,spdv-street, 131,WY,WYOMING +495,ddulq-name,crh-firstname, 11540,yrm-city,1974/07/31,opds-street, 95,OH,OHIO +496,fyrha-name,wea-firstname, 16620,vfe-city,1990/12/07,ukki-street, 48,GU,GUAM +497,vuypf-name,ugz-firstname, 13320,ixw-city,1970/07/09,aptu-street, 60,HI,HAWAII +498,wezwd-name,oae-firstname, 11180,egb-city,1982/02/27,ldea-street, 2,IL,ILLINOIS +499,wrokv-name,zaa-firstname, 13980,hac-city,1974/01/20,zwst-street, 21,MO,MISSOURI +500,wtapc-name,ciu-firstname, 15360,uvh-city,1962/08/03,mddz-street, 196,IA,IOWA +501,hdtsu-name,two-firstname, 19500,ick-city,1958/11/07,kipe-street, 42,DE,DELAWARE +502,lwprl-name,jaw-firstname, 14140,acy-city,1970/12/06,jhae-street, 129,MT,MONTANA +503,iwftp-name,jnp-firstname, 18800,axg-city,1978/10/13,rejy-street, 174,AZ,ARIZONA +504,qypws-name,fox-firstname, 19820,bzu-city,1966/01/02,owsq-street, 70,AR,ARKANSAS +505,pzjcf-name,ese-firstname, 12180,kjq-city,1958/11/21,xbyg-street, 12,AS,AMERICAN SAMOA +506,itzxd-name,erv-firstname, 10460,dsk-city,1978/06/20,baci-street, 151,OH,OHIO +507,jnpdw-name,zna-firstname, 11000,aqt-city,1966/05/27,pukm-street, 80,WY,WYOMING +508,dchwr-name,rxe-firstname, 19220,plm-city,1958/05/18,gkgx-street, 100,NH,NEW HAMPSHIRE +509,pcszz-name,rym-firstname, 15860,tml-city,1983/01/25,qrdz-street, 7,RI,RHODE ISLAND +510,fatdr-name,rcs-firstname, 17480,ajx-city,1958/09/18,nlal-street, 26,MD,MARYLAND +511,oblzo-name,wwl-firstname, 17280,hxs-city,1958/06/15,rnpa-street, 20,AR,ARKANSAS +512,nmflo-name,ljc-firstname, 19720,biq-city,1962/03/23,ypux-street, 197,OR,OREGON +513,ajhdh-name,iba-firstname, 16920,yru-city,1982/09/08,zedq-street, 148,MN,MINNESOTA +514,aiewz-name,gla-firstname, 19340,zvj-city,1986/01/12,blie-street, 116,MA,MASSACHUSETTS +515,tglnr-name,fob-firstname, 14300,ejm-city,1986/09/22,zazt-street, 152,WV,WEST VIRGINIA +516,tswnt-name,aal-firstname, 19940,jsw-city,1978/07/02,xnjc-street, 125,OK,OKLAHOMA +517,smukz-name,zim-firstname, 15260,pul-city,1974/09/26,furv-street, 45,IA,IOWA +518,aygln-name,qfk-firstname, 16700,bmu-city,1958/03/18,esys-street, 148,NE,NEBRASKA +519,kcuub-name,ffc-firstname, 10720,xnk-city,1958/02/23,cefn-street, 135,AL,ALABAMA +520,vsujm-name,yne-firstname, 11280,gdr-city,1966/02/12,hdah-street, 70,MS,MISSISSIPPI +521,tirxh-name,gpy-firstname, 19360,bai-city,1970/04/19,gznh-street, 33,KY,KENTUCKY +522,wlqnf-name,nnd-firstname, 16120,kij-city,1954/10/07,gorj-street, 85,ID,IDAHO +523,rynel-name,iaq-firstname, 13640,chy-city,1954/02/01,wbiu-street, 62,WV,WEST VIRGINIA +524,ohcvr-name,eod-firstname, 18240,lcc-city,1978/12/24,guca-street, 84,LA,LOUISIANA +525,gabiw-name,gtj-firstname, 17860,ezv-city,1978/01/18,jsyv-street, 143,ND,NORTH DAKOTA +526,ddajc-name,cab-firstname, 12920,fgz-city,1958/10/06,lkvp-street, 80,NV,NEVADA +527,kivtx-name,pbs-firstname, 17980,mso-city,1982/10/22,qden-street, 69,IN,INDIANA +528,tyial-name,mwb-firstname, 17080,pdf-city,1954/02/25,qyym-street, 48,MN,MINNESOTA +529,ipbyo-name,lcr-firstname, 12040,ygz-city,1978/03/01,qbqk-street, 117,ND,NORTH DAKOTA +530,tmkxn-name,jhl-firstname, 13460,xkh-city,1966/06/07,wkzu-street, 26,SD,SOUTH DAKOTA +531,cnkac-name,jxe-firstname, 13140,vgf-city,1974/08/24,sifo-street, 84,ID,IDAHO +532,zqnwe-name,ujv-firstname, 19020,vjy-city,1966/02/03,sfzz-street, 171,MN,MINNESOTA +533,aphod-name,vsz-firstname, 11440,spc-city,1962/07/28,alnl-street, 152,FM,FEDERATED STATES OF MICRONESIA +534,tjber-name,uhp-firstname, 19320,quo-city,1958/12/19,zgus-street, 173,FM,FEDERATED STATES OF MICRONESIA +535,itblo-name,lbp-firstname, 14840,rgn-city,1962/11/29,qmtb-street, 1,IN,INDIANA +536,navpk-name,wcs-firstname, 14400,nvr-city,1982/11/01,qlar-street, 126,AK,ALASKA +537,nkyot-name,zep-firstname, 17720,yim-city,1954/09/30,wwdl-street, 198,ND,NORTH DAKOTA +538,qebao-name,edw-firstname, 19220,tlh-city,1982/10/16,celb-street, 180,NJ,NEW JERSEY +539,bzmvi-name,roq-firstname, 10240,beg-city,1974/07/10,nnom-street, 71,KY,KENTUCKY +540,qtzxt-name,nau-firstname, 17240,fis-city,1990/05/27,fbex-street, 22,AR,ARKANSAS +541,jarpd-name,mce-firstname, 17060,rgw-city,1982/10/20,eqbd-street, 180,WI,WISCONSIN +542,ppaal-name,afq-firstname, 17300,fno-city,1982/10/01,zhby-street, 108,NY,NEW YORK +543,sejhl-name,qwc-firstname, 18000,sfv-city,1986/04/13,xhlx-street, 52,CO,COLORADO +544,qfvqk-name,mqn-firstname, 10540,rwu-city,1958/07/30,xwsy-street, 169,AL,ALABAMA +545,ndowg-name,lnm-firstname, 13820,llz-city,1954/08/11,niif-street, 55,KS,KANSAS +546,hutga-name,ztk-firstname, 19800,uci-city,1950/10/04,ywtn-street, 84,KS,KANSAS +547,mudul-name,qcs-firstname, 13520,agk-city,1962/04/20,scto-street, 200,CO,COLORADO +548,mnket-name,edm-firstname, 10040,jqb-city,1954/11/09,avkk-street, 23,VA,VIRGINIA +549,mbxbn-name,yuu-firstname, 18880,oho-city,1970/11/24,heug-street, 116,PA,PENNSYLVANIA +550,letcc-name,pyw-firstname, 10160,lzb-city,1966/06/11,wtul-street, 80,RI,RHODE ISLAND +551,jqovo-name,eir-firstname, 14120,bvr-city,1974/07/01,wdxz-street, 156,SD,SOUTH DAKOTA +552,jdkan-name,vxc-firstname, 15600,rqa-city,1962/02/07,vroo-street, 182,CO,COLORADO +553,crzen-name,vwe-firstname, 18800,msz-city,1962/10/17,xxbp-street, 165,IA,IOWA +554,nogoy-name,swt-firstname, 11580,npe-city,1978/04/15,uath-street, 55,NV,NEVADA +555,qtkar-name,cdv-firstname, 17020,opk-city,1950/07/26,bsnt-street, 17,AK,ALASKA +556,oigrk-name,zmz-firstname, 14200,zhd-city,1966/11/30,yeqk-street, 12,DC,DISTRICT OF COLUMBIA +557,qspmb-name,htl-firstname, 10940,oxl-city,1986/11/17,bqdp-street, 71,NH,NEW HAMPSHIRE +558,qktka-name,ces-firstname, 12840,xcq-city,1986/02/13,ddqq-street, 62,NE,NEBRASKA +559,bokzl-name,vfw-firstname, 14160,eyd-city,1970/02/15,ffrm-street, 26,IL,ILLINOIS +560,eksje-name,xxw-firstname, 18600,vtn-city,1962/12/09,nskq-street, 42,VA,VIRGINIA +561,avxuw-name,niq-firstname, 13520,cyx-city,1958/05/29,dxit-street, 167,CA,CALIFORNIA +562,xwskh-name,mud-firstname, 10900,bcd-city,1954/02/15,lqcd-street, 5,VA,VIRGINIA +563,jrlgi-name,qbl-firstname, 11780,xkb-city,1970/01/27,wfti-street, 101,CO,COLORADO +564,vvgef-name,qbi-firstname, 17580,dgu-city,1974/10/21,pkhw-street, 140,DE,DELAWARE +565,vbdsa-name,mqy-firstname, 19520,iqf-city,1958/06/24,gnhg-street, 23,AL,ALABAMA +566,mimxv-name,idp-firstname, 19280,acp-city,1986/02/10,wziw-street, 1,LA,LOUISIANA +567,ihvtb-name,jdf-firstname, 17540,xpg-city,1970/11/23,pjvm-street, 156,SC,SOUTH CAROLINA +568,wztgo-name,njm-firstname, 15200,btu-city,1978/10/30,ihdb-street, 25,OH,OHIO +569,unnnh-name,zry-firstname, 12300,upy-city,1982/09/15,nbjp-street, 91,GU,GUAM +570,qtsep-name,spo-firstname, 15940,ryi-city,1983/01/26,kvcq-street, 94,FM,FEDERATED STATES OF MICRONESIA +571,rznvs-name,cjh-firstname, 15180,duz-city,1974/05/12,igjl-street, 111,AL,ALABAMA +572,yrdmb-name,lvl-firstname, 12240,pzi-city,1962/11/10,kegb-street, 54,NY,NEW YORK +573,jdhso-name,hms-firstname, 12620,ova-city,1950/02/14,rxks-street, 115,MS,MISSISSIPPI +574,seigu-name,rwp-firstname, 15620,yrt-city,1958/09/30,mloc-street, 9,NJ,NEW JERSEY +575,wzura-name,fzo-firstname, 12500,vgt-city,1974/06/01,tevl-street, 50,NC,NORTH CAROLINA +576,urqhu-name,hiq-firstname, 15020,ejs-city,1966/04/08,khev-street, 38,AK,ALASKA +577,yazce-name,zhy-firstname, 12920,ytz-city,1982/05/07,rfsh-street, 125,NH,NEW HAMPSHIRE +578,okxei-name,qbi-firstname, 17400,plc-city,1966/04/05,mvzs-street, 73,KS,KANSAS +579,eemhh-name,nsj-firstname, 11660,buq-city,1986/06/23,mftt-street, 191,NE,NEBRASKA +580,uyoei-name,wiz-firstname, 19000,lqo-city,1974/06/18,rvat-street, 197,NC,NORTH CAROLINA +581,ahbez-name,gwg-firstname, 10940,gno-city,1978/03/23,rfwr-street, 105,KS,KANSAS +582,qnopb-name,xgc-firstname, 18800,ayx-city,1962/05/30,lrnc-street, 193,TN,TENNESSEE +583,beqpz-name,psc-firstname, 10880,sfe-city,1986/07/04,tobm-street, 121,AK,ALASKA +584,nnvir-name,tbu-firstname, 12860,cuj-city,1962/04/25,wuxg-street, 11,WA,WASHINGTON +585,cmqrt-name,huk-firstname, 19580,kae-city,1958/09/29,yukq-street, 146,IA,IOWA +586,ifivh-name,bub-firstname, 15640,pti-city,1950/04/21,alaw-street, 104,VT,VERMONT +587,nqbqs-name,ndv-firstname, 17500,sxr-city,1963/01/05,zclw-street, 77,MP,NORTHERN MARIANA ISLANDS +588,kgrlf-name,suz-firstname, 16080,ayf-city,1990/03/07,vmto-street, 120,CO,COLORADO +589,nhjsx-name,xuh-firstname, 12360,tqo-city,1970/08/04,vfzt-street, 181,undefined,undefined +590,vdint-name,zal-firstname, 14600,fjh-city,1982/04/22,uopg-street, 117,WV,WEST VIRGINIA +591,nchqr-name,ldr-firstname, 14700,pyy-city,1966/03/02,nlzm-street, 137,VT,VERMONT +592,sjmbq-name,tsc-firstname, 19960,dlp-city,1974/05/26,qphd-street, 155,IN,INDIANA +593,njbsk-name,oft-firstname, 17280,xtc-city,1958/02/13,sebl-street, 85,HI,HAWAII +594,vtijb-name,obv-firstname, 14000,bpn-city,1962/08/08,mxrl-street, 199,IL,ILLINOIS +595,ihqjn-name,hnu-firstname, 18960,ztv-city,1982/02/20,mhrl-street, 45,MA,MASSACHUSETTS +596,kpzko-name,zfz-firstname, 18720,rjc-city,1971/01/01,ycrf-street, 173,CT,CONNECTICUT +597,qlfgm-name,fnv-firstname, 11360,avw-city,1970/12/01,yvgg-street, 49,LA,LOUISIANA +598,oeepm-name,asq-firstname, 18660,mvq-city,1990/07/29,ssey-street, 57,OH,OHIO +599,waklo-name,ksz-firstname, 10660,ddf-city,1958/07/10,dilr-street, 145,NY,NEW YORK +600,mdftt-name,ldq-firstname, 11460,mkm-city,1966/09/03,rjkt-street, 9,CA,CALIFORNIA +601,iowsr-name,vmg-firstname, 18420,kie-city,1954/12/30,qfom-street, 117,NE,NEBRASKA +602,sdtxj-name,zbp-firstname, 16160,gic-city,1962/09/09,eavm-street, 108,IA,IOWA +603,ocebd-name,img-firstname, 12180,rxp-city,1966/08/18,ygmy-street, 52,PA,PENNSYLVANIA +604,uvovz-name,nsd-firstname, 13600,tlc-city,1950/08/13,xbsm-street, 110,MP,NORTHERN MARIANA ISLANDS +605,fzuuf-name,pub-firstname, 10500,ikg-city,1974/04/26,pmnl-street, 189,MA,MASSACHUSETTS +606,xzvbs-name,qpb-firstname, 17060,xed-city,1950/04/18,vvdx-street, 118,OR,OREGON +607,qvkir-name,hns-firstname, 12840,kxi-city,1978/03/02,kzbt-street, 65,RI,RHODE ISLAND +608,llarb-name,psp-firstname, 11240,wlq-city,1990/10/10,qshw-street, 106,AL,ALABAMA +609,wrzts-name,ygm-firstname, 15520,ybe-city,1974/12/03,uxct-street, 200,ND,NORTH DAKOTA +610,qfpdp-name,luy-firstname, 16260,qma-city,1978/07/12,opsm-street, 194,RI,RHODE ISLAND +611,xaosj-name,kfd-firstname, 18100,xmf-city,1982/12/22,hceh-street, 75,LA,LOUISIANA +612,iuocr-name,ztn-firstname, 19000,sby-city,1970/06/07,qres-street, 86,TX,TEXAS +613,ocvrm-name,ylm-firstname, 16360,vif-city,1986/11/17,zgov-street, 17,CT,CONNECTICUT +614,tealr-name,lbu-firstname, 10020,blg-city,1958/03/23,qjvy-street, 182,MS,MISSISSIPPI +615,ksjja-name,dky-firstname, 15360,qim-city,1986/08/30,rvtm-street, 182,KY,KENTUCKY +616,jbhpa-name,shc-firstname, 18020,gmc-city,1974/05/29,cvpl-street, 154,AS,AMERICAN SAMOA +617,vraon-name,nam-firstname, 16320,joa-city,1986/11/07,pnik-street, 194,NM,NEW MEXICO +618,gtymv-name,lgo-firstname, 16740,ynr-city,1970/05/14,xxzj-street, 74,NJ,NEW JERSEY +619,sxbpo-name,hyx-firstname, 12900,lvu-city,1958/07/14,tysz-street, 68,HI,HAWAII +620,yaogj-name,fjg-firstname, 11040,slr-city,1974/06/22,kjoi-street, 134,OK,OKLAHOMA +621,rprrj-name,vca-firstname, 19160,txr-city,1959/01/18,stjh-street, 137,MA,MASSACHUSETTS +622,dbaaq-name,jdi-firstname, 13800,uge-city,1954/11/08,ftck-street, 175,OK,OKLAHOMA +623,eapsq-name,zeu-firstname, 14720,mmh-city,1954/11/01,tjlj-street, 84,AS,AMERICAN SAMOA +624,wlrqb-name,srf-firstname, 15280,bbq-city,1970/04/06,thvb-street, 17,CO,COLORADO +625,dzzia-name,ukn-firstname, 14680,zug-city,1954/04/29,nthu-street, 109,LA,LOUISIANA +626,nfzxa-name,ikc-firstname, 17080,sqt-city,1982/01/21,dteu-street, 113,CT,CONNECTICUT +627,ovxiz-name,lho-firstname, 10620,gox-city,1954/07/05,cofd-street, 155,NY,NEW YORK +628,msubu-name,ccj-firstname, 17780,jkv-city,1982/02/25,gfve-street, 43,NV,NEVADA +629,xxbdr-name,kas-firstname, 17320,ick-city,1970/02/25,gvoi-street, 24,ID,IDAHO +630,rgryb-name,guv-firstname, 11160,xus-city,1966/05/06,plka-street, 164,FL,FLORIDA +631,dxprz-name,byy-firstname, 16520,lpb-city,1974/03/01,auoq-street, 103,MH,MARSHALL ISLANDS +632,nerdb-name,eqa-firstname, 12860,arg-city,1958/03/17,emcz-street, 104,DC,DISTRICT OF COLUMBIA +633,nszhh-name,nrn-firstname, 16860,hap-city,1974/04/30,zjuq-street, 78,IA,IOWA +634,jtcxq-name,hxi-firstname, 12160,oqq-city,1982/06/03,xpsp-street, 30,VI,VIRGIN ISLANDS +635,prglc-name,kqd-firstname, 16080,gqr-city,1978/10/14,pjin-street, 145,NH,NEW HAMPSHIRE +636,yofsk-name,qch-firstname, 14240,wbz-city,1990/05/28,edsh-street, 5,MI,MICHIGAN +637,cupcm-name,jvy-firstname, 13720,lkw-city,1966/10/14,frzx-street, 83,NY,NEW YORK +638,crjjf-name,yfe-firstname, 19880,oof-city,1970/02/07,akkj-street, 98,NC,NORTH CAROLINA +639,rqyyg-name,hbx-firstname, 12080,sxh-city,1958/04/24,wzjb-street, 159,TX,TEXAS +640,xxugu-name,yqf-firstname, 10380,ocx-city,1954/11/18,mbps-street, 20,ND,NORTH DAKOTA +641,ayvhk-name,znx-firstname, 16960,qbw-city,1954/07/23,wgsh-street, 148,OK,OKLAHOMA +642,xwdxq-name,aik-firstname, 14280,djc-city,1958/06/18,mjpa-street, 130,OH,OHIO +643,qsvmf-name,lgn-firstname, 17140,mpl-city,1982/06/15,ngot-street, 193,NJ,NEW JERSEY +644,tlued-name,mcz-firstname, 16780,oab-city,1978/09/22,ppzd-street, 111,AR,ARKANSAS +645,ibhei-name,djd-firstname, 11340,eep-city,1975/01/24,rzdt-street, 94,MH,MARSHALL ISLANDS +646,snfrd-name,jof-firstname, 12700,lck-city,1978/12/16,jpcu-street, 184,IN,INDIANA +647,vkwed-name,elj-firstname, 18940,tdi-city,1990/08/05,ekyx-street, 137,KY,KENTUCKY +648,acing-name,rxv-firstname, 10660,gtt-city,1970/11/17,ltxb-street, 117,VA,VIRGINIA +649,fzmkk-name,ilb-firstname, 18540,rwj-city,1986/02/08,xsnm-street, 113,MA,MASSACHUSETTS +650,knzxd-name,seg-firstname, 17180,oqu-city,1982/11/02,ukda-street, 80,MT,MONTANA +651,zzyix-name,dlk-firstname, 14180,yoc-city,1986/10/06,jnpv-street, 71,FM,FEDERATED STATES OF MICRONESIA +652,nsote-name,ivj-firstname, 19740,aqh-city,1990/11/05,faox-street, 77,WI,WISCONSIN +653,znees-name,edg-firstname, 18120,qkr-city,1986/12/08,prpo-street, 42,DE,DELAWARE +654,svzbm-name,hsg-firstname, 19180,shh-city,1954/02/05,uglh-street, 150,IA,IOWA +655,llfur-name,geu-firstname, 12340,qdf-city,1970/12/01,nntt-street, 92,AZ,ARIZONA +656,pusrl-name,ovn-firstname, 17020,oaa-city,1954/07/30,oxfp-street, 104,AL,ALABAMA +657,wihqb-name,bkq-firstname, 13540,jau-city,1974/09/25,vuga-street, 83,NY,NEW YORK +658,ezgdt-name,yaa-firstname, 13380,fdc-city,1986/11/04,yxut-street, 75,VI,VIRGIN ISLANDS +659,kvtlv-name,zdr-firstname, 15700,bqd-city,1990/06/04,kajd-street, 92,DE,DELAWARE +660,bqkzl-name,tym-firstname, 14900,rhk-city,1962/02/13,yyzk-street, 165,MH,MARSHALL ISLANDS +661,ndfve-name,ajt-firstname, 19460,tdr-city,1970/02/04,vpub-street, 143,CT,CONNECTICUT +662,temdh-name,trc-firstname, 11720,ald-city,1950/05/06,tapb-street, 1,FL,FLORIDA +663,bzagi-name,dwa-firstname, 13580,zvq-city,1970/05/05,khyi-street, 105,CT,CONNECTICUT +664,frtvn-name,ukn-firstname, 19400,eto-city,1986/06/29,dcnz-street, 114,MH,MARSHALL ISLANDS +665,mjdqu-name,xlw-firstname, 14420,lpe-city,1962/08/02,oecb-street, 189,IN,INDIANA +666,fnopj-name,sdh-firstname, 19080,hxm-city,1974/11/24,pfxz-street, 66,ME,MAINE +667,kuxaz-name,hye-firstname, 14860,eia-city,1962/09/16,nhdg-street, 198,CO,COLORADO +668,tfrpe-name,mme-firstname, 12860,mox-city,1978/03/24,uhqo-street, 10,NE,NEBRASKA +669,eodve-name,tzj-firstname, 11440,syb-city,1986/07/29,jlnw-street, 74,WI,WISCONSIN +670,slzsi-name,nvw-firstname, 13680,kgz-city,1958/07/13,pptx-street, 88,NY,NEW YORK +671,wrttz-name,wwx-firstname, 13440,lig-city,1986/11/05,vdph-street, 66,MI,MICHIGAN +672,wfxhy-name,wtc-firstname, 12760,gnf-city,1970/08/22,itut-street, 3,HI,HAWAII +673,toeja-name,vrw-firstname, 18900,xes-city,1982/09/01,wpqu-street, 184,undefined,undefined +674,glmnt-name,bck-firstname, 11680,cuu-city,1978/08/02,wtxv-street, 16,RI,RHODE ISLAND +675,vyvnk-name,shl-firstname, 10260,xhq-city,1970/06/06,ddbz-street, 28,MA,MASSACHUSETTS +676,pjdvs-name,adv-firstname, 15600,snf-city,1986/12/10,tdft-street, 133,DC,DISTRICT OF COLUMBIA +677,rulrt-name,ytn-firstname, 19340,spb-city,1974/11/11,bpwx-street, 24,VA,VIRGINIA +678,gvttx-name,moj-firstname, 17000,gri-city,1978/11/24,kqlt-street, 27,ID,IDAHO +679,jtlah-name,gxq-firstname, 15440,jte-city,1978/02/01,mbvv-street, 172,OH,OHIO +680,cpvhs-name,ruo-firstname, 16080,vhq-city,1970/10/18,ditw-street, 164,AR,ARKANSAS +681,jikyx-name,hzl-firstname, 13320,hgh-city,1958/08/07,gmqp-street, 85,FL,FLORIDA +682,bsvsn-name,idb-firstname, 15780,ooc-city,1958/04/28,apff-street, 78,MO,MISSOURI +683,qgivn-name,bau-firstname, 11580,uez-city,1987/01/29,vpsa-street, 48,RI,RHODE ISLAND +684,hqbfl-name,fgr-firstname, 11620,ejc-city,1982/02/07,fwsq-street, 107,WA,WASHINGTON +685,crmtk-name,fmd-firstname, 14480,mdp-city,1962/05/23,knxw-street, 18,IL,ILLINOIS +686,kccgj-name,pzd-firstname, 10760,ifv-city,1990/05/14,dgfl-street, 172,PR,PUERTO RICO +687,mddbw-name,lcu-firstname, 11360,crz-city,1970/08/24,eiwf-street, 32,PR,PUERTO RICO +688,dlnxj-name,tzx-firstname, 11380,nza-city,1966/09/25,jlna-street, 186,MO,MISSOURI +689,wpjcx-name,hwx-firstname, 13100,slr-city,1987/01/16,wxcr-street, 42,AK,ALASKA +690,yefnh-name,jzp-firstname, 13080,ozf-city,1986/08/09,zzos-street, 120,OR,OREGON +691,qsoei-name,kio-firstname, 11860,esa-city,1974/08/04,bhbl-street, 86,HI,HAWAII +692,xwodh-name,aid-firstname, 17940,wgd-city,1954/07/21,rcrf-street, 90,IL,ILLINOIS +693,oncpb-name,elk-firstname, 12020,rbp-city,1974/10/28,iblz-street, 114,DE,DELAWARE +694,qisar-name,kfc-firstname, 19800,alw-city,1958/08/07,ukbj-street, 53,NH,NEW HAMPSHIRE +695,bvsga-name,qln-firstname, 18060,zlt-city,1966/01/29,rngf-street, 46,WA,WASHINGTON +696,xguku-name,uxf-firstname, 10080,ezg-city,1974/12/30,icvq-street, 48,UT,UTAH +697,hxylt-name,hng-firstname, 15360,jgc-city,1966/03/11,lhxd-street, 87,DE,DELAWARE +698,dmlyv-name,qai-firstname, 20000,hoe-city,1966/08/20,uboj-street, 72,KY,KENTUCKY +699,kkbjx-name,cnx-firstname, 16780,ndu-city,1970/08/16,afzw-street, 55,NJ,NEW JERSEY +700,emoio-name,awr-firstname, 18560,qpj-city,1974/11/17,hasx-street, 92,VT,VERMONT +701,lxpni-name,oox-firstname, 14100,xrj-city,1986/06/09,wvvi-street, 106,MO,MISSOURI +702,oshuu-name,mim-firstname, 14280,mns-city,1982/09/12,ashj-street, 95,FM,FEDERATED STATES OF MICRONESIA +703,uhpxg-name,gnt-firstname, 19180,zsi-city,1978/05/22,namc-street, 126,AL,ALABAMA +704,lvoek-name,reg-firstname, 18920,ubj-city,1954/12/24,nrjw-street, 38,PA,PENNSYLVANIA +705,lpnhx-name,szp-firstname, 10720,ybd-city,1970/11/26,ntyc-street, 109,GA,GEORGIA +706,kppvg-name,ztz-firstname, 14660,ocp-city,1990/08/19,ekcr-street, 56,SD,SOUTH DAKOTA +707,vxwxv-name,qzo-firstname, 15540,rfl-city,1962/03/29,hcwy-street, 116,KY,KENTUCKY +708,nbgan-name,ocf-firstname, 15240,aas-city,1954/09/14,zkkh-street, 117,IN,INDIANA +709,faiaw-name,lup-firstname, 16980,rsz-city,1986/09/17,yddi-street, 98,IN,INDIANA +710,mdmax-name,ggz-firstname, 17360,stn-city,1966/11/25,lwhf-street, 198,IN,INDIANA +711,etauo-name,pta-firstname, 13360,fnh-city,1978/02/03,gsrn-street, 113,IA,IOWA +712,gckpz-name,rou-firstname, 17920,liu-city,1974/10/10,apjm-street, 40,AZ,ARIZONA +713,szvhz-name,ani-firstname, 17240,nab-city,1958/12/10,cxho-street, 87,KY,KENTUCKY +714,dawkl-name,epe-firstname, 11780,pjg-city,1990/04/24,hdbx-street, 104,WI,WISCONSIN +715,eifes-name,zka-firstname, 11500,uux-city,1986/10/20,pazo-street, 125,TN,TENNESSEE +716,fphzw-name,gof-firstname, 14680,dkz-city,1986/06/23,aejx-street, 88,VA,VIRGINIA +717,bgicm-name,one-firstname, 14820,lwh-city,1966/01/08,dizm-street, 53,AR,ARKANSAS +718,qnozr-name,fwa-firstname, 17800,rsm-city,1954/04/11,tuun-street, 63,NE,NEBRASKA +719,zxcjb-name,mhs-firstname, 18620,czu-city,1982/11/15,ucfg-street, 103,PW,PALAU +720,qqftw-name,dox-firstname, 11280,eii-city,1982/08/03,akjj-street, 174,VT,VERMONT +721,byevj-name,bah-firstname, 14240,rge-city,1958/01/21,rkkm-street, 142,MD,MARYLAND +722,ihvvb-name,gaq-firstname, 19140,gua-city,1958/09/30,hxim-street, 88,RI,RHODE ISLAND +723,suiee-name,vji-firstname, 12120,zuz-city,1970/02/22,ijcn-street, 84,PW,PALAU +724,fgkoi-name,xkx-firstname, 15880,zuk-city,1970/12/27,frfg-street, 74,IL,ILLINOIS +725,aowhf-name,hvv-firstname, 12740,mpj-city,1987/01/06,mrmx-street, 95,KY,KENTUCKY +726,vxkbl-name,xfk-firstname, 18600,odo-city,1970/02/28,jrzc-street, 128,AZ,ARIZONA +727,zpedp-name,oxh-firstname, 16380,cjz-city,1958/05/28,bnxj-street, 18,GU,GUAM +728,uqfju-name,dmq-firstname, 15560,ehf-city,1954/03/02,ptqn-street, 68,VA,VIRGINIA +729,wsoxm-name,xtd-firstname, 15660,ogk-city,1951/01/29,inkh-street, 150,WV,WEST VIRGINIA +730,cvypk-name,xoy-firstname, 15080,cwr-city,1970/02/28,kyio-street, 117,WV,WEST VIRGINIA +731,yprgp-name,vgc-firstname, 14660,alm-city,1971/01/05,uoxp-street, 110,CA,CALIFORNIA +732,udhil-name,dsa-firstname, 12060,lzv-city,1974/02/18,gvvc-street, 149,AK,ALASKA +733,fgtjr-name,bnt-firstname, 18240,pgf-city,1986/10/15,synp-street, 159,CT,CONNECTICUT +734,hypgs-name,ycz-firstname, 10840,ufx-city,1966/06/26,frur-street, 81,HI,HAWAII +735,ijqas-name,bgl-firstname, 18680,dss-city,1950/02/20,rafi-street, 134,ID,IDAHO +736,seeae-name,pvg-firstname, 10800,txd-city,1958/10/18,uibx-street, 102,GU,GUAM +737,punzk-name,ubx-firstname, 15020,qzq-city,1978/07/13,cqww-street, 52,PA,PENNSYLVANIA +738,xyvjy-name,jta-firstname, 19700,koe-city,1986/06/22,lrjw-street, 17,undefined,undefined +739,ysnek-name,kop-firstname, 13120,bel-city,1950/08/01,xcbk-street, 94,MA,MASSACHUSETTS +740,ozxng-name,sje-firstname, 15180,nqf-city,1986/11/19,uivr-street, 117,MP,NORTHERN MARIANA ISLANDS +741,ekqcg-name,hxq-firstname, 16880,jic-city,1966/01/11,grbw-street, 86,UT,UTAH +742,xxhts-name,krw-firstname, 14140,lwz-city,1978/04/06,fzbe-street, 40,WI,WISCONSIN +743,uffha-name,jdu-firstname, 13080,jmd-city,1986/06/21,sqbk-street, 66,KS,KANSAS +744,aevbo-name,bba-firstname, 16880,hgh-city,1958/03/08,urgb-street, 74,NE,NEBRASKA +745,rqwnb-name,zpb-firstname, 19300,cbx-city,1978/08/12,qavq-street, 5,IN,INDIANA +746,txozw-name,vet-firstname, 15540,ajt-city,1954/07/05,jayh-street, 105,RI,RHODE ISLAND +747,xmyzv-name,phl-firstname, 15220,jnk-city,1974/04/04,ckwm-street, 174,KY,KENTUCKY +748,fusnk-name,iva-firstname, 14660,kqd-city,1982/07/03,zmbq-street, 10,FL,FLORIDA +749,ksnje-name,eaq-firstname, 12500,gej-city,1982/10/03,vbwg-street, 83,HI,HAWAII +750,kcahb-name,srs-firstname, 11300,bvz-city,1978/05/16,gyfr-street, 164,LA,LOUISIANA +751,bzjht-name,vfn-firstname, 17500,mgi-city,1958/06/15,mmko-street, 172,DE,DELAWARE +752,loydq-name,gft-firstname, 11900,fsr-city,1990/01/20,sjds-street, 62,TN,TENNESSEE +753,kglxv-name,zlt-firstname, 14960,qgb-city,1982/02/07,gniz-street, 178,AS,AMERICAN SAMOA +754,loimm-name,ipg-firstname, 15980,sxt-city,1978/10/31,kdlk-street, 22,NY,NEW YORK +755,demtf-name,yhq-firstname, 12300,pdo-city,1982/09/18,mfye-street, 99,TN,TENNESSEE +756,jdmdi-name,llu-firstname, 18060,pss-city,1974/07/01,utrs-street, 198,CA,CALIFORNIA +757,crjfv-name,ccn-firstname, 19520,uub-city,1978/07/12,xtvk-street, 138,OK,OKLAHOMA +758,irghm-name,dbi-firstname, 15200,otd-city,1982/12/14,iple-street, 144,WY,WYOMING +759,mcwlu-name,kxy-firstname, 17640,edu-city,1958/07/05,ibtg-street, 173,IN,INDIANA +760,plfvg-name,cxa-firstname, 19360,rnc-city,1958/04/06,zemc-street, 64,DE,DELAWARE +761,dnxai-name,gnd-firstname, 11580,ers-city,1966/07/14,ytjt-street, 163,OH,OHIO +762,ksbwq-name,tku-firstname, 10040,axu-city,1974/08/10,kvel-street, 95,RI,RHODE ISLAND +763,durgp-name,unr-firstname, 12820,juk-city,1959/01/05,ucky-street, 132,HI,HAWAII +764,eocqj-name,qcg-firstname, 18940,faf-city,1986/10/14,gjjy-street, 126,OK,OKLAHOMA +765,dhcim-name,eti-firstname, 17140,aed-city,1982/07/19,syxk-street, 156,FL,FLORIDA +766,nerhu-name,jfx-firstname, 10140,huk-city,1978/02/07,cyfs-street, 47,ME,MAINE +767,sxukr-name,hag-firstname, 14400,lfc-city,1963/01/05,wylb-street, 38,WV,WEST VIRGINIA +768,xznte-name,snc-firstname, 17840,mhl-city,1962/03/23,rfig-street, 151,DE,DELAWARE +769,tgwig-name,rfn-firstname, 14020,fcs-city,1986/11/12,agpo-street, 79,undefined,undefined +770,kmkbd-name,fzv-firstname, 12920,kjt-city,1974/03/03,sgjo-street, 63,MN,MINNESOTA +771,kqzne-name,gvz-firstname, 15500,zar-city,1966/04/01,kvlk-street, 35,DC,DISTRICT OF COLUMBIA +772,hatuj-name,fwt-firstname, 16040,gih-city,1978/07/10,wbjj-street, 70,MD,MARYLAND +773,anvjr-name,qqu-firstname, 15280,php-city,1970/09/28,hhnz-street, 147,GU,GUAM +774,jxiox-name,ein-firstname, 18560,vou-city,1974/04/29,dlcm-street, 52,VA,VIRGINIA +775,ahdci-name,fhq-firstname, 10040,dis-city,1974/05/17,flyu-street, 84,MO,MISSOURI +776,slvkr-name,qtq-firstname, 10800,seu-city,1978/08/24,hivm-street, 148,PR,PUERTO RICO +777,xncyt-name,alj-firstname, 12500,qcq-city,1962/05/20,bqjo-street, 78,VI,VIRGIN ISLANDS +778,oagkd-name,oud-firstname, 10720,nai-city,1974/09/12,jbps-street, 50,MT,MONTANA +779,bhinx-name,sde-firstname, 18240,zrt-city,1962/07/08,dxuq-street, 97,PA,PENNSYLVANIA +780,wpyvx-name,qwg-firstname, 18520,lku-city,1966/03/09,ymkt-street, 133,MD,MARYLAND +781,wvkan-name,ndd-firstname, 10340,hwn-city,1978/11/18,pqqf-street, 114,NV,NEVADA +782,biapu-name,kiq-firstname, 16360,lbi-city,1954/10/27,pawe-street, 198,OK,OKLAHOMA +783,ohygc-name,vxw-firstname, 11100,flm-city,1966/11/20,homq-street, 170,HI,HAWAII +784,ntbgw-name,msg-firstname, 13740,cth-city,1958/09/28,epdz-street, 80,AS,AMERICAN SAMOA +785,vegli-name,avd-firstname, 18000,gob-city,1970/08/27,bvnt-street, 32,WY,WYOMING +786,aadoy-name,gre-firstname, 16040,qln-city,1962/01/03,lqxs-street, 179,ME,MAINE +787,rzwid-name,xjl-firstname, 16300,eds-city,1970/05/08,hutz-street, 27,TX,TEXAS +788,qperl-name,nsk-firstname, 13920,hxo-city,1966/11/16,xvzr-street, 160,IA,IOWA +789,hkhzc-name,hxd-firstname, 19980,gpm-city,1954/08/23,qmrm-street, 60,VA,VIRGINIA +790,qlixr-name,tsd-firstname, 16780,dvv-city,1978/02/14,ibwa-street, 85,AZ,ARIZONA +791,ieyhf-name,oox-firstname, 19820,mfp-city,1958/02/17,ntdb-street, 100,MD,MARYLAND +792,xxtwm-name,zpp-firstname, 12980,udl-city,1970/03/01,dkzb-street, 157,DC,DISTRICT OF COLUMBIA +793,hvtaa-name,wcr-firstname, 14460,zek-city,1970/01/18,bayt-street, 149,PR,PUERTO RICO +794,xduae-name,iqk-firstname, 18380,lkm-city,1982/11/24,aujb-street, 62,GA,GEORGIA +795,qkjcz-name,abw-firstname, 15080,fco-city,1986/11/01,tkuc-street, 69,FM,FEDERATED STATES OF MICRONESIA +796,jplol-name,akr-firstname, 19760,rvj-city,1970/12/23,sqxe-street, 128,SD,SOUTH DAKOTA +797,iegsb-name,ujo-firstname, 18480,abx-city,1974/02/24,esda-street, 16,RI,RHODE ISLAND +798,ypqix-name,hbp-firstname, 13240,kdv-city,1986/05/20,iyaq-street, 10,OH,OHIO +799,awluy-name,ysq-firstname, 13680,kcv-city,1986/12/16,rtbk-street, 144,ID,IDAHO +800,vtyuq-name,cup-firstname, 13200,gcq-city,1970/11/05,omwn-street, 156,AR,ARKANSAS +801,hmvzq-name,jgx-firstname, 18660,hbj-city,1982/03/30,hxik-street, 59,MO,MISSOURI +802,vhatl-name,ywe-firstname, 10040,ytj-city,1982/02/10,lblz-street, 62,CT,CONNECTICUT +803,pvozw-name,pln-firstname, 13980,tdu-city,1986/06/18,ulqg-street, 8,MT,MONTANA +804,mwmvk-name,ttp-firstname, 14020,dcw-city,1982/08/26,lqrt-street, 104,CO,COLORADO +805,pmlsn-name,uxe-firstname, 17680,udv-city,1962/12/27,usnv-street, 155,PR,PUERTO RICO +806,uuvrf-name,jxu-firstname, 16360,vpo-city,1978/08/04,tpez-street, 25,MS,MISSISSIPPI +807,pvcck-name,nnx-firstname, 19780,kfm-city,1962/07/05,aybu-street, 151,CA,CALIFORNIA +808,kdybn-name,qcg-firstname, 11080,wno-city,1978/05/06,omzx-street, 56,GU,GUAM +809,ylapy-name,mou-firstname, 11400,yky-city,1990/03/12,rcbx-street, 167,WI,WISCONSIN +810,ygvfd-name,hqp-firstname, 13240,sfw-city,1958/05/16,svut-street, 16,RI,RHODE ISLAND +811,wgbsf-name,wgj-firstname, 12000,itm-city,1966/07/31,xlwg-street, 122,NV,NEVADA +812,guhzx-name,hde-firstname, 19260,kqz-city,1962/03/01,escf-street, 84,NY,NEW YORK +813,quyjf-name,vta-firstname, 14440,yiw-city,1978/01/29,nsss-street, 83,ND,NORTH DAKOTA +814,fsbhc-name,ink-firstname, 19220,qjo-city,1974/08/30,pwtn-street, 140,WA,WASHINGTON +815,zhzwu-name,pkl-firstname, 17680,slh-city,1966/06/13,cspv-street, 106,NJ,NEW JERSEY +816,sqegy-name,ufg-firstname, 19220,hry-city,1954/03/25,anrd-street, 173,PW,PALAU +817,bkiln-name,sjs-firstname, 15100,fao-city,1958/09/04,inav-street, 170,AR,ARKANSAS +818,zynej-name,zgh-firstname, 15100,spc-city,1962/01/25,zewf-street, 85,GU,GUAM +819,xzasj-name,kib-firstname, 18080,rzn-city,1970/10/07,tlsi-street, 190,SD,SOUTH DAKOTA +820,tmqlc-name,jes-firstname, 11560,vhc-city,1974/11/27,xxkh-street, 36,MO,MISSOURI +821,gowim-name,rcr-firstname, 11200,kja-city,1974/06/06,zhkg-street, 41,MO,MISSOURI +822,bulti-name,cse-firstname, 13660,ukw-city,1970/12/14,glrc-street, 59,NV,NEVADA +823,evxmh-name,gfc-firstname, 13560,cbo-city,1986/03/05,sfkz-street, 72,ID,IDAHO +824,dbdop-name,swu-firstname, 14760,tlv-city,1982/10/06,vprj-street, 87,AR,ARKANSAS +825,mgyeq-name,wko-firstname, 15320,lxz-city,1990/03/22,mfwa-street, 11,DE,DELAWARE +826,bfplt-name,ard-firstname, 10360,sou-city,1990/02/13,jniu-street, 124,NC,NORTH CAROLINA +827,dvbot-name,auq-firstname, 16200,qkn-city,1978/03/20,qfzt-street, 9,IL,ILLINOIS +828,tncjm-name,kvn-firstname, 11640,mlf-city,1954/02/08,igos-street, 166,ID,IDAHO +829,svoxr-name,rbc-firstname, 10400,bny-city,1958/07/13,socq-street, 198,MH,MARSHALL ISLANDS +830,jxzxn-name,wau-firstname, 14360,orx-city,1958/07/08,fssh-street, 77,NM,NEW MEXICO +831,dglke-name,dyc-firstname, 13100,lgz-city,1982/12/18,qixe-street, 158,MH,MARSHALL ISLANDS +832,pewvm-name,asq-firstname, 10840,czn-city,1966/04/02,wnac-street, 84,MD,MARYLAND +833,tfgsc-name,vxn-firstname, 10100,wup-city,1982/02/05,qlsb-street, 193,AK,ALASKA +834,hmoxn-name,vti-firstname, 13660,gvq-city,1974/11/25,dahy-street, 123,HI,HAWAII +835,bexku-name,mvy-firstname, 13080,wrx-city,1966/09/25,ovag-street, 86,GA,GEORGIA +836,ksffn-name,ubg-firstname, 18600,gsu-city,1958/02/03,vepx-street, 58,NE,NEBRASKA +837,ngfsu-name,pgq-firstname, 10400,pag-city,1986/06/19,rwqz-street, 123,DE,DELAWARE +838,mopuh-name,vdg-firstname, 14160,xbe-city,1970/11/22,vbak-street, 103,MA,MASSACHUSETTS +839,rifdh-name,eky-firstname, 12900,uwr-city,1978/04/02,yysd-street, 81,GU,GUAM +840,avlwf-name,xpn-firstname, 14940,ebu-city,1974/08/06,vcki-street, 154,PW,PALAU +841,avokq-name,dzw-firstname, 14680,xnx-city,1954/02/23,cmis-street, 175,NH,NEW HAMPSHIRE +842,nahim-name,yvv-firstname, 18020,egx-city,1978/05/05,rpeq-street, 86,NJ,NEW JERSEY +843,kehud-name,gus-firstname, 13200,bwc-city,1962/02/19,aagh-street, 166,OK,OKLAHOMA +844,tkfjt-name,kch-firstname, 11160,vuw-city,1974/07/20,mkli-street, 119,WY,WYOMING +845,gcwwn-name,rpb-firstname, 10640,yum-city,1982/05/17,vqgz-street, 157,OR,OREGON +846,piobc-name,pii-firstname, 17360,vcs-city,1950/06/02,obsu-street, 105,FM,FEDERATED STATES OF MICRONESIA +847,nfguz-name,rtb-firstname, 14380,zrh-city,1982/03/01,jltk-street, 164,PW,PALAU +848,kayxc-name,gew-firstname, 15780,miz-city,1959/01/22,bckb-street, 85,ME,MAINE +849,kwjrg-name,lwy-firstname, 19080,qym-city,1986/08/26,fagw-street, 181,OR,OREGON +850,joacl-name,nay-firstname, 15120,wdm-city,1982/12/10,goug-street, 6,VT,VERMONT +851,fmnme-name,ctx-firstname, 13940,qnj-city,1954/04/12,cacp-street, 155,AL,ALABAMA +852,cqjtz-name,vqp-firstname, 16480,san-city,1966/10/16,oijg-street, 94,NY,NEW YORK +853,tdndt-name,qya-firstname, 16320,log-city,1962/05/08,pvtk-street, 151,MP,NORTHERN MARIANA ISLANDS +854,cquwe-name,lzs-firstname, 12940,vnf-city,1986/08/16,tsfg-street, 181,SD,SOUTH DAKOTA +855,dbfxb-name,glb-firstname, 18420,ecz-city,1978/11/28,fpuk-street, 42,PW,PALAU +856,odwcc-name,tcz-firstname, 17420,ffa-city,1954/06/12,kirb-street, 158,WI,WISCONSIN +857,qctkr-name,wpo-firstname, 16980,upo-city,1978/06/21,gfrm-street, 116,TX,TEXAS +858,qikks-name,eep-firstname, 18560,tof-city,1986/08/11,trqf-street, 64,WA,WASHINGTON +859,gbltk-name,jny-firstname, 12080,tdu-city,1978/09/15,wsii-street, 133,ID,IDAHO +860,cirdu-name,dzw-firstname, 11340,stm-city,1974/04/04,qnat-street, 48,PA,PENNSYLVANIA +861,xukav-name,rep-firstname, 18240,pzu-city,1950/11/16,piru-street, 24,PW,PALAU +862,dqxqx-name,eca-firstname, 16080,ddl-city,1982/08/15,snln-street, 109,AK,ALASKA +863,jxcxv-name,ipw-firstname, 19200,ctv-city,1982/12/03,zazz-street, 171,TN,TENNESSEE +864,ddgxr-name,bwx-firstname, 18860,giq-city,1954/05/11,ukyp-street, 161,SC,SOUTH CAROLINA +865,wcboj-name,cqb-firstname, 15080,srk-city,1978/04/28,pycv-street, 12,PW,PALAU +866,ifgzt-name,bde-firstname, 17700,whw-city,1978/05/05,qfgv-street, 162,SD,SOUTH DAKOTA +867,riwqf-name,qil-firstname, 18480,mce-city,1990/09/07,qxyc-street, 69,MP,NORTHERN MARIANA ISLANDS +868,zdudw-name,yxg-firstname, 17380,apv-city,1954/09/09,wymd-street, 195,KS,KANSAS +869,szvfk-name,nei-firstname, 13660,xqq-city,1974/10/14,ygbr-street, 129,KY,KENTUCKY +870,lrgna-name,lqv-firstname, 16840,woh-city,1978/09/16,ibzb-street, 87,IL,ILLINOIS +871,jeaqx-name,kdj-firstname, 16840,lpn-city,1979/01/26,guxb-street, 10,WA,WASHINGTON +872,ubvth-name,njr-firstname, 14920,hbz-city,1966/10/20,knnm-street, 69,AL,ALABAMA +873,opkuq-name,sbc-firstname, 10520,wpu-city,1974/10/24,tzoa-street, 10,OK,OKLAHOMA +874,eeesy-name,sei-firstname, 13420,xhc-city,1978/01/10,wsmi-street, 48,AK,ALASKA +875,njzrw-name,yep-firstname, 13680,hdj-city,1974/05/12,bhba-street, 42,ME,MAINE +876,caria-name,jaf-firstname, 18660,scx-city,1962/12/18,envv-street, 158,GU,GUAM +877,sdwui-name,dax-firstname, 17780,mdr-city,1958/12/22,orah-street, 167,AK,ALASKA +878,pnklv-name,not-firstname, 11160,gfk-city,1982/08/07,muqk-street, 6,AZ,ARIZONA +879,xhqwk-name,cgz-firstname, 19940,nji-city,1962/05/06,ihqj-street, 97,TN,TENNESSEE +880,oigoy-name,ccp-firstname, 14060,ase-city,1958/05/20,faef-street, 133,MD,MARYLAND +881,nysrz-name,nil-firstname, 17700,ile-city,1966/09/13,aqiy-street, 25,WI,WISCONSIN +882,wrkvm-name,ftf-firstname, 17780,wim-city,1982/12/12,kzed-street, 57,MN,MINNESOTA +883,zngvz-name,tcp-firstname, 11640,vba-city,1982/05/29,phle-street, 105,OK,OKLAHOMA +884,wdubl-name,his-firstname, 19840,ybx-city,1978/08/06,upqg-street, 80,TN,TENNESSEE +885,fxvpc-name,yxz-firstname, 13380,ocv-city,1958/04/08,shtt-street, 28,IL,ILLINOIS +886,blbvw-name,nln-firstname, 17740,gwm-city,1955/01/28,ratd-street, 98,UT,UTAH +887,ajxnx-name,utp-firstname, 15540,eak-city,1950/07/01,zlzg-street, 74,OR,OREGON +888,aeeid-name,bmc-firstname, 19100,qtd-city,1958/10/28,sgwc-street, 127,OH,OHIO +889,ypwhc-name,ojx-firstname, 11480,zib-city,1967/01/09,gpfp-street, 76,UT,UTAH +890,kuxdp-name,exg-firstname, 14840,cvh-city,1986/01/22,jbif-street, 108,TN,TENNESSEE +891,ujwmw-name,aqz-firstname, 14080,mwz-city,1978/07/22,eggm-street, 8,MN,MINNESOTA +892,xogup-name,pps-firstname, 11740,bic-city,1970/11/26,thtu-street, 34,ME,MAINE +893,vmrty-name,rmg-firstname, 11060,cna-city,1970/08/27,uknc-street, 24,GA,GEORGIA +894,rrwco-name,dji-firstname, 18380,oln-city,1974/01/02,fzqy-street, 129,MI,MICHIGAN +895,dvcbl-name,nam-firstname, 15040,sbp-city,1962/03/02,txma-street, 170,HI,HAWAII +896,tbsrx-name,krm-firstname, 15220,gao-city,1959/01/03,fmhf-street, 125,TX,TEXAS +897,ymjve-name,nmc-firstname, 19280,yux-city,1974/12/11,ycqn-street, 65,FL,FLORIDA +898,rycyz-name,prd-firstname, 15920,oml-city,1982/09/13,ivie-street, 30,OK,OKLAHOMA +899,agxhw-name,zla-firstname, 14780,pkw-city,1970/04/28,mcrt-street, 139,MN,MINNESOTA +900,bbdii-name,wkd-firstname, 15220,eup-city,1962/05/01,neqn-street, 191,ID,IDAHO +901,svszz-name,gyt-firstname, 12900,sav-city,1954/09/30,gqnm-street, 117,SC,SOUTH CAROLINA +902,dqidv-name,ooi-firstname, 17340,krq-city,1978/06/24,hjtm-street, 106,NJ,NEW JERSEY +903,blcfn-name,lrz-firstname, 19240,sav-city,1978/03/01,xrfr-street, 8,RI,RHODE ISLAND +904,acgjp-name,ncp-firstname, 17900,snk-city,1974/10/27,qbrs-street, 65,IA,IOWA +905,eloeo-name,tmh-firstname, 17500,use-city,1971/01/05,hkrt-street, 23,PA,PENNSYLVANIA +906,lxogw-name,hlp-firstname, 10960,yhi-city,1990/10/25,ewnk-street, 7,GU,GUAM +907,gvwtl-name,dmy-firstname, 19920,fqc-city,1970/05/02,mpze-street, 63,LA,LOUISIANA +908,vrrxx-name,xmg-firstname, 17160,yks-city,1978/08/14,hikr-street, 165,PW,PALAU +909,hkpuu-name,ofx-firstname, 17040,hid-city,1978/02/03,rkrn-street, 8,UT,UTAH +910,bupib-name,ftw-firstname, 16620,fsw-city,1971/01/09,uzxm-street, 95,RI,RHODE ISLAND +911,siwci-name,fir-firstname, 11180,sqp-city,1974/06/27,pgeq-street, 134,AS,AMERICAN SAMOA +912,mpkrp-name,etl-firstname, 10560,kty-city,1958/06/02,cpug-street, 67,CA,CALIFORNIA +913,itfnp-name,ncv-firstname, 16440,dgw-city,1958/07/29,xdkl-street, 131,OK,OKLAHOMA +914,oyvqm-name,lwu-firstname, 14760,dzo-city,1958/07/09,ubqh-street, 57,IA,IOWA +915,kuila-name,umo-firstname, 11880,rhy-city,1982/09/17,jwha-street, 81,AK,ALASKA +916,zduzo-name,gmh-firstname, 12360,oxv-city,1990/06/26,dbxf-street, 76,VI,VIRGIN ISLANDS +917,cxggf-name,fld-firstname, 12900,sif-city,1954/09/08,wdis-street, 142,WI,WISCONSIN +918,euelo-name,sbi-firstname, 19340,shq-city,1986/04/20,qmja-street, 3,GA,GEORGIA +919,papgo-name,oyu-firstname, 10280,bet-city,1970/06/19,otzt-street, 155,CA,CALIFORNIA +920,dtjeu-name,gvg-firstname, 11980,aie-city,1954/10/17,tcpt-street, 77,NY,NEW YORK +921,xryzl-name,oqb-firstname, 16540,era-city,1962/08/27,aywk-street, 118,VI,VIRGIN ISLANDS +922,pydqj-name,rwh-firstname, 15640,ekn-city,1978/09/16,yecx-street, 30,WA,WASHINGTON +923,picbf-name,cru-firstname, 16040,iaa-city,1986/07/01,ttsg-street, 112,MO,MISSOURI +924,jfqgk-name,ghq-firstname, 10420,yap-city,1958/06/19,qzbd-street, 1,NV,NEVADA +925,iqoex-name,dha-firstname, 17340,fqq-city,1966/05/09,mbkz-street, 151,VT,VERMONT +926,xlugu-name,fae-firstname, 12060,uvm-city,1954/11/21,dhin-street, 100,CT,CONNECTICUT +927,pxbfc-name,cia-firstname, 15160,xrr-city,1986/06/20,psch-street, 122,MT,MONTANA +928,koqls-name,teq-firstname, 16100,vtl-city,1982/04/28,qfxy-street, 11,ME,MAINE +929,nrhco-name,cwj-firstname, 17380,zzw-city,1958/06/19,dcir-street, 188,WA,WASHINGTON +930,rrxbl-name,gfc-firstname, 13160,ruo-city,1982/06/02,yers-street, 108,MH,MARSHALL ISLANDS +931,rqtri-name,oqi-firstname, 19780,jvk-city,1970/12/08,dfnt-street, 196,IA,IOWA +932,txjky-name,gvy-firstname, 19040,jwu-city,1982/03/01,pacb-street, 83,UT,UTAH +933,efrbq-name,tgu-firstname, 13200,xgk-city,1970/04/10,ipjk-street, 156,WI,WISCONSIN +934,xzrxi-name,lwt-firstname, 16980,zll-city,1982/01/27,geic-street, 42,LA,LOUISIANA +935,uafzk-name,wox-firstname, 10240,agj-city,1978/07/03,wdwx-street, 13,TX,TEXAS +936,npomk-name,upa-firstname, 19060,oad-city,1986/06/27,qkua-street, 200,MP,NORTHERN MARIANA ISLANDS +937,bepdw-name,box-firstname, 13280,sxr-city,1978/04/15,rnub-street, 19,ME,MAINE +938,qbbcf-name,igs-firstname, 15440,xrs-city,1958/09/13,yhum-street, 176,MA,MASSACHUSETTS +939,hirau-name,vyi-firstname, 15980,iyv-city,1962/09/17,gqqy-street, 48,FM,FEDERATED STATES OF MICRONESIA +940,fzpiq-name,rco-firstname, 10660,wpc-city,1978/03/30,kktg-street, 149,NY,NEW YORK +941,kvtgo-name,wkl-firstname, 13840,yik-city,1962/02/18,goyb-street, 15,NV,NEVADA +942,mpljt-name,xyx-firstname, 14880,kdx-city,1982/05/25,fkgb-street, 50,WV,WEST VIRGINIA +943,cxjtz-name,zkx-firstname, 18500,mrg-city,1974/09/04,seup-street, 139,MP,NORTHERN MARIANA ISLANDS +944,cbkvb-name,uji-firstname, 13260,rtg-city,1954/02/08,oedv-street, 5,AZ,ARIZONA +945,qsujg-name,hig-firstname, 11980,obk-city,1974/12/24,awap-street, 140,WI,WISCONSIN +946,faqty-name,wml-firstname, 19180,qmg-city,1958/07/06,zxlq-street, 9,NV,NEVADA +947,cdwge-name,aeu-firstname, 16940,axh-city,1970/12/16,nkhw-street, 165,RI,RHODE ISLAND +948,ctmij-name,tqq-firstname, 12600,apv-city,1990/12/10,egyw-street, 94,MD,MARYLAND +949,axcvk-name,dhn-firstname, 11900,osf-city,1982/03/17,tzwx-street, 71,OH,OHIO +950,flhxd-name,tty-firstname, 10380,nfn-city,1970/04/16,iweq-street, 144,MN,MINNESOTA +951,bnnjj-name,ttq-firstname, 11360,shi-city,1966/11/16,xhde-street, 125,MI,MICHIGAN +952,ohowr-name,rlf-firstname, 16380,dzs-city,1982/05/12,yiwv-street, 46,IL,ILLINOIS +953,ofqhs-name,pyy-firstname, 12100,pfy-city,1958/04/28,xqtd-street, 148,UT,UTAH +954,quyhm-name,oln-firstname, 12140,lqe-city,1978/09/28,prlu-street, 151,MO,MISSOURI +955,ailpr-name,cut-firstname, 13200,yqu-city,1950/07/05,fxsk-street, 101,IA,IOWA +956,zeyok-name,vhb-firstname, 17240,nih-city,1986/11/30,bvms-street, 86,WA,WASHINGTON +957,xrfau-name,owh-firstname, 17180,tep-city,1954/03/02,qcmo-street, 45,NJ,NEW JERSEY +958,nuoiw-name,rxs-firstname, 11320,uef-city,1962/04/23,oiuf-street, 117,MA,MASSACHUSETTS +959,imjdp-name,mlh-firstname, 19020,mpi-city,1962/06/28,obxd-street, 11,PA,PENNSYLVANIA +960,ynmgr-name,uzp-firstname, 15500,oxs-city,1986/07/15,epkx-street, 92,GA,GEORGIA +961,wcucp-name,qlt-firstname, 12460,sow-city,1986/05/17,qoxd-street, 40,WV,WEST VIRGINIA +962,ugiex-name,ebp-firstname, 15640,vgo-city,1974/09/28,ggdc-street, 22,MH,MARSHALL ISLANDS +963,nsilq-name,vxf-firstname, 19280,zzm-city,1986/03/13,kxrw-street, 68,VA,VIRGINIA +964,whtok-name,oqs-firstname, 18340,ddf-city,1954/10/13,toaz-street, 100,VT,VERMONT +965,lbwzb-name,rgg-firstname, 15340,bla-city,1978/12/10,usrd-street, 175,TN,TENNESSEE +966,lcbat-name,nnd-firstname, 16200,ebl-city,1990/11/20,emud-street, 144,MI,MICHIGAN +967,alouv-name,omr-firstname, 14980,ouw-city,1958/06/11,xbnb-street, 90,AZ,ARIZONA +968,nvqwv-name,blm-firstname, 17880,nip-city,1974/12/14,sgzi-street, 33,AS,AMERICAN SAMOA +969,wdidq-name,qwd-firstname, 15480,eis-city,1979/01/22,qqbv-street, 43,NH,NEW HAMPSHIRE +970,jmlfw-name,gal-firstname, 12680,fjt-city,1955/01/10,iqnm-street, 81,GU,GUAM +971,feoxv-name,mme-firstname, 18020,oed-city,1986/01/13,jtud-street, 73,NV,NEVADA +972,gsdoz-name,oxc-firstname, 10420,jff-city,1978/02/20,jnom-street, 62,OR,OREGON +973,hklot-name,rur-firstname, 13900,nur-city,1986/06/05,nkmx-street, 195,SD,SOUTH DAKOTA +974,smemp-name,ehc-firstname, 10080,wrv-city,1950/04/10,wfrv-street, 93,FL,FLORIDA +975,htgft-name,nzy-firstname, 16960,gwe-city,1966/05/09,abiz-street, 58,UT,UTAH +976,jaikt-name,tls-firstname, 17280,whv-city,1966/09/19,wfzd-street, 70,ME,MAINE +977,yshns-name,zhh-firstname, 13200,dty-city,1978/12/13,hiht-street, 158,KS,KANSAS +978,selvf-name,zcr-firstname, 12900,phm-city,1958/12/28,pont-street, 149,GU,GUAM +979,qoxyo-name,oth-firstname, 10280,uic-city,1986/03/16,havi-street, 153,ID,IDAHO +980,uabxl-name,cid-firstname, 18000,cea-city,1982/02/18,jykd-street, 127,WV,WEST VIRGINIA +981,hjnpb-name,wwh-firstname, 12400,fwd-city,1978/10/01,ryqy-street, 101,OH,OHIO +982,zcqum-name,ecp-firstname, 15820,rpu-city,1958/11/11,fpoi-street, 118,undefined,undefined +983,cetkk-name,btl-firstname, 18660,xrq-city,1970/04/26,mkuo-street, 167,TX,TEXAS +984,laofd-name,fqf-firstname, 18180,ddy-city,1986/04/01,qjar-street, 14,OR,OREGON +985,xjtie-name,tpq-firstname, 16800,fgb-city,1970/03/09,nqpk-street, 108,MA,MASSACHUSETTS +986,txrwc-name,ygc-firstname, 17880,ipb-city,1962/04/26,zomj-street, 189,DC,DISTRICT OF COLUMBIA +987,csicd-name,yzm-firstname, 18240,nwu-city,1978/05/03,ishz-street, 24,LA,LOUISIANA +988,njtnl-name,nxa-firstname, 10360,ols-city,1990/12/01,joem-street, 101,DE,DELAWARE +989,dclnv-name,hve-firstname, 17080,amp-city,1978/05/22,jfuv-street, 17,WI,WISCONSIN +990,jrazh-name,yec-firstname, 14000,tsi-city,1986/09/02,guzz-street, 119,KS,KANSAS +991,hpxko-name,psn-firstname, 11900,ocf-city,1970/04/21,gvbf-street, 12,OK,OKLAHOMA +992,lggcw-name,sey-firstname, 17260,ike-city,1958/09/19,yrnf-street, 34,NE,NEBRASKA +993,cxifr-name,agu-firstname, 14360,ujj-city,1954/09/28,widd-street, 127,undefined,undefined +994,kfwgh-name,qaa-firstname, 17900,kis-city,1970/10/02,wwgu-street, 134,MD,MARYLAND +995,pcizt-name,fdt-firstname, 15320,tzn-city,1982/09/08,nqii-street, 163,TN,TENNESSEE +996,hyskd-name,ott-firstname, 19800,zja-city,1962/03/05,tjpp-street, 108,OH,OHIO +997,nfdtl-name,jxu-firstname, 12980,isx-city,1982/07/05,dldc-street, 166,CA,CALIFORNIA +998,xtukc-name,aih-firstname, 15360,ctn-city,1974/11/10,qhwy-street, 102,MI,MICHIGAN +999,qfwcj-name,fdu-firstname, 10100,enp-city,1950/01/11,qqga-street, 19,TX,TEXAS +1000,uvhph-name,vri-firstname, 11920,tjs-city,1974/04/28,czih-street, 151,NE,NEBRASKA + diff --git a/integration-tests/spark-native/datasets/0099-state-data-input.csv b/integration-tests/spark-native/datasets/0099-state-data-input.csv new file mode 100644 index 00000000000..54cc72a4f70 --- /dev/null +++ b/integration-tests/spark-native/datasets/0099-state-data-input.csv @@ -0,0 +1,52 @@ +state,population +Alabama,4874747 +ALASKA,739795 +Arizona,7016270 +Arkansas,3004279 +CALIFORNIA,39536653 +Colorado,5607154 +Connecticut,3588184 +Delaware,961939 +District of Columbia,693972 +FLORIDA,20984400 +Georgia,10429379 +Hawaii,1427538 +Idaho,1716943 +Illinois,12802023 +INDIANA,6666818 +Iowa,3145711 +Kansas,2913123 +Kentucky,4454189 +Louisiana,4684333 +Maine,1335907 +Maryland,6052177 +Massachusetts,6859819 +Michigan,9962311 +Minnesota,5576606 +Mississippi,2984100 +Missouri,6113532 +Montana,1050493 +NEBRASKA,1920076 +Nevada,2998039 +New Hampshire,1342795 +New Jersey,9005644 +New Mexico,2088070 +NEW YORK,19849399 +North Carolina,10273419 +North Dakota,755393 +Ohio,11658609 +Oklahoma,3930864 +Oregon,4142776 +Pennsylvania,12805537 +Rhode Island,1059639 +South Carolina,5024369 +South Dakota,869666 +Tennessee,6715984 +TEXAS,28304596 +Utah,3101833 +Vermont,623657 +Virginia,8470020 +WASHINGTON,7405743 +West Virginia,1815857 +Wisconsin,5795483 +Wyoming,579315 diff --git a/integration-tests/spark-native/dev-env-config.json b/integration-tests/spark-native/dev-env-config.json new file mode 100644 index 00000000000..16b13b09d6a --- /dev/null +++ b/integration-tests/spark-native/dev-env-config.json @@ -0,0 +1,3 @@ +{ + "variables": [] +} diff --git a/integration-tests/spark-native/files/customers-1k.txt b/integration-tests/spark-native/files/customers-1k.txt new file mode 100644 index 00000000000..cd8e3b01b29 --- /dev/null +++ b/integration-tests/spark-native/files/customers-1k.txt @@ -0,0 +1,1001 @@ +id;name;firstname;zip;city;birthdate;street;housenr;stateCode;state + 1;jwcdf-name;fsj-firstname; 13520;oem-city;1954/02/07;amrb-street; 145;AK;ALASKA + 2;flhxu-name;tum-firstname; 17520;buo-city;1966/04/24;wfyz-street; 96;GA;GEORGIA + 3;xthfg-name;gfe-firstname; 12560;vtz-city;1990/01/11;doxx-street; 46;NJ;NEW JERSEY + 4;ulzrz-name;bnl-firstname; 11620;prz-city;1966/08/02;bxqn-street; 104;NY;NEW YORK + 5;oxhyr-name;onx-firstname; 15180;bpn-city;1970/11/14;pksn-street; 133;IN;INDIANA + 6;fiqjz-name;sce-firstname; 16020;fnn-city;1954/09/24;wbhg-street; 35;MD;MARYLAND + 7;tkiat-name;xti-firstname; 12720;stt-city;1966/08/11;tvnf-street; 21;PA;PENNSYLVANIA + 8;kljcz-name;uqd-firstname; 13340;ntt-city;1987/01/15;jyje-street; 10;PW;PALAU + 9;pgunz-name;hcm-firstname; 16680;gxh-city;1970/11/08;shbe-street; 184;NC;NORTH CAROLINA + 10;oyjha-name;uhj-firstname; 18880;uyg-city;1966/04/10;bjgw-street; 176;AR;ARKANSAS + 11;igxbd-name;uph-firstname; 13480;ndh-city;1962/12/03;jdcd-street; 151;NH;NEW HAMPSHIRE + 12;vnaov-name;wha-firstname; 13120;egm-city;1954/03/28;hpep-street; 20;CA;CALIFORNIA + 13;dauuz-name;hwg-firstname; 13740;khn-city;1958/05/15;etqx-street; 5;OK;OKLAHOMA + 14;gkuuo-name;kkb-firstname; 13560;xdt-city;1962/04/07;sdoj-street; 35;MT;MONTANA + 15;wdhze-name;jjk-firstname; 16900;due-city;1970/07/17;pmmu-street; 174;AS;AMERICAN SAMOA + 16;ncayz-name;ynb-firstname; 15720;lxj-city;1974/04/27;mdtb-street; 109;MA;MASSACHUSETTS + 17;rdjin-name;hhu-firstname; 14480;lpc-city;1958/11/16;wxik-street; 145;KY;KENTUCKY + 18;nxzij-name;bdl-firstname; 10740;avx-city;1958/02/20;nybz-street; 138;WI;WISCONSIN + 19;xgrzc-name;dxw-firstname; 18900;vpq-city;1990/11/16;wzjh-street; 58;ME;MAINE + 20;ehgrn-name;vbe-firstname; 17500;cik-city;1978/05/21;ucnw-street; 135;MD;MARYLAND + 21;gctjx-name;upx-firstname; 11960;yqr-city;1958/03/03;rlko-street; 141;TN;TENNESSEE + 22;ptzmg-name;hva-firstname; 15740;gux-city;1978/05/04;pugy-street; 122;VI;VIRGIN ISLANDS + 23;eyeti-name;gnw-firstname; 17420;eko-city;1962/10/26;ylph-street; 61;NC;NORTH CAROLINA + 24;wccwo-name;zpj-firstname; 16600;uim-city;1962/09/29;ygih-street; 26;WA;WASHINGTON + 25;bwkoe-name;ayl-firstname; 18660;rtw-city;1978/07/16;mzww-street; 179;CA;CALIFORNIA + 26;rezku-name;zio-firstname; 19080;nvt-city;1982/07/14;wwkd-street; 91;CA;CALIFORNIA + 27;mjlsk-name;ecx-firstname; 10800;yxu-city;1950/12/11;vttb-street; 195;MO;MISSOURI + 28;wdjsi-name;aoq-firstname; 13660;smo-city;1954/02/01;kako-street; 7;NV;NEVADA + 29;mwfnd-name;nyb-firstname; 19760;bbu-city;1986/09/23;apdi-street; 91;MS;MISSISSIPPI + 30;vtuoz-name;jhh-firstname; 17620;vad-city;1982/05/05;kzup-street; 79;GA;GEORGIA + 31;rhhxk-name;ndr-firstname; 16760;fub-city;1978/11/12;regd-street; 55;OK;OKLAHOMA + 32;lpstk-name;mqz-firstname; 18940;tnr-city;1982/09/16;cdhf-street; 4;SD;SOUTH DAKOTA + 33;ldhyr-name;yts-firstname; 12000;auk-city;1986/11/14;abph-street; 147;IN;INDIANA + 34;cjdml-name;iti-firstname; 16900;wkq-city;1970/06/05;npow-street; 96;NH;NEW HAMPSHIRE + 35;cpenz-name;sbi-firstname; 16380;ssl-city;1962/08/19;kilz-street; 44;MS;MISSISSIPPI + 36;rxtbg-name;anr-firstname; 14720;bqc-city;1958/08/10;pudg-street; 140;NV;NEVADA + 37;udblf-name;raa-firstname; 11500;wli-city;1978/12/13;xomd-street; 41;PW;PALAU + 38;vvyce-name;gep-firstname; 13740;gtd-city;1982/05/23;kwbv-street; 123;undefined;undefined + 39;kwfnz-name;ucu-firstname; 10580;sns-city;1978/08/18;nnun-street; 20;OK;OKLAHOMA + 40;zxydx-name;tml-firstname; 14680;jda-city;1974/05/29;wfjn-street; 157;DC;DISTRICT OF COLUMBIA + 41;bfscx-name;jnl-firstname; 16920;yyg-city;1970/11/30;cgfh-street; 178;CO;COLORADO + 42;qitur-name;yra-firstname; 15560;ijp-city;1978/01/30;fonc-street; 155;AK;ALASKA + 43;msixi-name;ynb-firstname; 12720;ksl-city;1958/07/17;zpjw-street; 46;VI;VIRGIN ISLANDS + 44;wzkjq-name;rgh-firstname; 19000;hkm-city;1974/08/12;yixf-street; 134;CA;CALIFORNIA + 45;dqfmf-name;yxr-firstname; 13840;vie-city;1962/10/23;stvx-street; 39;TX;TEXAS + 46;biluz-name;uqe-firstname; 17760;wkq-city;1962/07/27;embn-street; 183;PW;PALAU + 47;wahfx-name;zwd-firstname; 13240;vic-city;1974/03/27;axpw-street; 131;UT;UTAH + 48;denwt-name;bta-firstname; 17300;hhj-city;1986/12/20;orwy-street; 11;WV;WEST VIRGINIA + 49;akdmy-name;ybz-firstname; 14560;wtx-city;1962/11/08;nwba-street; 123;MP;NORTHERN MARIANA ISLANDS + 50;hqafg-name;nht-firstname; 16080;gfu-city;1951/01/12;spsq-street; 45;LA;LOUISIANA + 51;zhmbl-name;lnw-firstname; 17460;hse-city;1986/12/21;scis-street; 97;GA;GEORGIA + 52;snwnj-name;jyy-firstname; 16400;hsz-city;1966/02/15;imhl-street; 42;NC;NORTH CAROLINA + 53;fuyla-name;mmp-firstname; 11840;hgu-city;1986/08/16;ixiz-street; 145;NC;NORTH CAROLINA + 54;yvfqz-name;prz-firstname; 11260;wjl-city;1982/05/06;fbzd-street; 97;MO;MISSOURI + 55;usbgq-name;vhd-firstname; 14080;dsb-city;1958/04/01;ggoc-street; 54;KS;KANSAS + 56;yaeni-name;zpy-firstname; 19100;sen-city;1954/12/10;sbsw-street; 158;HI;HAWAII + 57;fgxvr-name;vzi-firstname; 17520;lcf-city;1958/11/01;nbdv-street; 10;GU;GUAM + 58;tqpbq-name;rwr-firstname; 19140;zpd-city;1978/08/23;npvb-street; 190;DC;DISTRICT OF COLUMBIA + 59;ieigg-name;ayq-firstname; 12960;ljc-city;1962/07/05;dnjz-street; 163;FL;FLORIDA + 60;rfvzu-name;edm-firstname; 13340;kvz-city;1954/12/08;eijd-street; 4;RI;RHODE ISLAND + 61;pduwm-name;gqb-firstname; 14240;cyr-city;1954/07/03;ndux-street; 13;SD;SOUTH DAKOTA + 62;yyixf-name;yzt-firstname; 18020;lwx-city;1974/01/29;iede-street; 120;NV;NEVADA + 63;dkszq-name;ytd-firstname; 14700;zwh-city;1979/01/11;nbjz-street; 65;AS;AMERICAN SAMOA + 64;slkzv-name;zbg-firstname; 19880;oee-city;1978/11/01;sphg-street; 119;OK;OKLAHOMA + 65;nvxim-name;phc-firstname; 19220;vgg-city;1991/01/24;juok-street; 106;FM;FEDERATED STATES OF MICRONESIA + 66;piyfg-name;xtn-firstname; 13760;nde-city;1954/07/22;vfrv-street; 11;NY;NEW YORK + 67;jnusz-name;mjw-firstname; 12640;nwb-city;1986/08/23;kcsa-street; 138;VA;VIRGINIA + 68;jnypj-name;ioq-firstname; 17000;zqy-city;1986/01/09;croe-street; 119;PW;PALAU + 69;uohts-name;btx-firstname; 13480;dal-city;1990/10/22;llyw-street; 150;WA;WASHINGTON + 70;aavpj-name;pvw-firstname; 13780;lai-city;1954/09/23;nygu-street; 171;FL;FLORIDA + 71;nbjcj-name;rsf-firstname; 12000;kjl-city;1986/06/30;ijsb-street; 123;ID;IDAHO + 72;syjxh-name;gkq-firstname; 19960;rmd-city;1978/10/26;qmyp-street; 161;MN;MINNESOTA + 73;vkojz-name;ryo-firstname; 14300;bmz-city;1954/09/11;gcpj-street; 71;ND;NORTH DAKOTA + 74;pqzfw-name;kld-firstname; 16400;qvq-city;1962/09/09;dhbv-street; 92;ND;NORTH DAKOTA + 75;owvjk-name;fez-firstname; 19740;ldb-city;1978/06/14;kabf-street; 87;VA;VIRGINIA + 76;qsfih-name;ixe-firstname; 16860;qvr-city;1987/01/07;qean-street; 159;CO;COLORADO + 77;slixq-name;gmb-firstname; 19980;ftt-city;1982/06/22;xinx-street; 111;VT;VERMONT + 78;eegsa-name;xlc-firstname; 12680;byk-city;1954/04/23;beul-street; 56;MD;MARYLAND + 79;phevp-name;ihs-firstname; 16120;adc-city;1978/04/25;voig-street; 98;NM;NEW MEXICO + 80;njfoe-name;tag-firstname; 16580;tnr-city;1966/12/04;dhky-street; 108;LA;LOUISIANA + 81;bdncx-name;hcd-firstname; 11260;xcl-city;1970/07/02;jvlp-street; 49;GA;GEORGIA + 82;ikedo-name;tks-firstname; 17460;odl-city;1958/08/25;iaaq-street; 8;GU;GUAM + 83;iafxy-name;vur-firstname; 11480;hgt-city;1962/08/03;hmec-street; 164;TX;TEXAS + 84;lafhf-name;ssz-firstname; 19560;wwp-city;1951/01/25;mxmq-street; 96;IN;INDIANA + 85;okyny-name;hbu-firstname; 16800;yok-city;1978/03/28;ipjz-street; 135;NV;NEVADA + 86;hznby-name;fwy-firstname; 13680;wbi-city;1970/07/25;mxui-street; 170;CT;CONNECTICUT + 87;ztpoa-name;rzk-firstname; 18500;qum-city;1970/07/26;blqr-street; 152;ME;MAINE + 88;gitxz-name;axt-firstname; 11800;fck-city;1974/01/12;tmjw-street; 189;SD;SOUTH DAKOTA + 89;ziomm-name;mcv-firstname; 12940;iwq-city;1950/10/22;hqgj-street; 140;DC;DISTRICT OF COLUMBIA + 90;otncg-name;tuy-firstname; 16540;ulk-city;1971/01/24;yuia-street; 166;TX;TEXAS + 91;cnabb-name;hoq-firstname; 16300;tuw-city;1962/06/17;ujvv-street; 61;ME;MAINE + 92;ucogf-name;ggc-firstname; 14500;fsj-city;1978/02/08;asfi-street; 53;WV;WEST VIRGINIA + 93;lbpmf-name;sdt-firstname; 10780;ewj-city;1978/03/08;hxsp-street; 102;NV;NEVADA + 94;tieqq-name;uyu-firstname; 17740;wea-city;1966/10/31;abpl-street; 187;MO;MISSOURI + 95;fsgwf-name;vjd-firstname; 12460;ads-city;1970/11/29;yeou-street; 10;MA;MASSACHUSETTS + 96;reeba-name;kzs-firstname; 13100;zhc-city;1966/07/08;abmv-street; 88;FL;FLORIDA + 97;shybc-name;gcp-firstname; 10660;ahg-city;1950/12/15;hrqy-street; 174;KS;KANSAS + 98;phszr-name;sst-firstname; 13080;ydd-city;1954/09/23;quqn-street; 2;RI;RHODE ISLAND + 99;jteco-name;fxc-firstname; 19760;agr-city;1986/05/06;dzxc-street; 108;MD;MARYLAND + 100;qvaar-name;icx-firstname; 16120;boc-city;1978/08/04;bfzf-street; 12;NM;NEW MEXICO + 101;iiapu-name;veo-firstname; 10180;wdv-city;1954/05/29;ovyu-street; 55;WI;WISCONSIN + 102;qfawh-name;wlx-firstname; 11280;fad-city;1962/04/28;hibx-street; 188;MP;NORTHERN MARIANA ISLANDS + 103;nfurq-name;rib-firstname; 17080;xcp-city;1962/11/10;rqui-street; 164;MI;MICHIGAN + 104;hdbiw-name;wxm-firstname; 12600;txy-city;1978/11/23;yfcx-street; 112;OK;OKLAHOMA + 105;oyher-name;jws-firstname; 17900;bai-city;1978/12/30;tyil-street; 178;NC;NORTH CAROLINA + 106;fzjnb-name;wxk-firstname; 12300;fda-city;1974/03/26;aweg-street; 7;MO;MISSOURI + 107;ycpcp-name;xfq-firstname; 12660;mna-city;1986/02/14;dcki-street; 5;AZ;ARIZONA + 108;rxxeb-name;qdw-firstname; 17600;yks-city;1970/11/15;zvsf-street; 74;MH;MARSHALL ISLANDS + 109;ffbgl-name;fqf-firstname; 11680;npo-city;1974/12/07;vvan-street; 175;GA;GEORGIA + 110;foygy-name;vog-firstname; 16920;mun-city;1970/07/03;urct-street; 153;HI;HAWAII + 111;imoqe-name;xfe-firstname; 14620;gfv-city;1986/02/20;nlak-street; 181;AK;ALASKA + 112;mnhwk-name;bbt-firstname; 16180;bnf-city;1986/10/10;yybd-street; 144;AL;ALABAMA + 113;lkfyf-name;xhg-firstname; 13260;myb-city;1958/10/27;jjcn-street; 128;GA;GEORGIA + 114;bkhya-name;hrh-firstname; 11500;byw-city;1990/03/03;oubr-street; 41;MH;MARSHALL ISLANDS + 115;avceb-name;ztu-firstname; 10020;ogo-city;1974/05/26;iuob-street; 31;KS;KANSAS + 116;pnacg-name;iws-firstname; 11640;dtx-city;1966/06/01;kwwj-street; 177;WV;WEST VIRGINIA + 117;ypnsc-name;tyv-firstname; 16820;glg-city;1962/12/19;nysz-street; 13;OK;OKLAHOMA + 118;agndn-name;qae-firstname; 15540;tpg-city;1990/05/15;ygzx-street; 166;NV;NEVADA + 119;qqhon-name;tlb-firstname; 19880;iif-city;1982/02/05;vsbj-street; 191;MP;NORTHERN MARIANA ISLANDS + 120;qzxic-name;mot-firstname; 14840;qvf-city;1970/03/19;gelo-street; 149;WA;WASHINGTON + 121;lunca-name;ced-firstname; 13700;wjp-city;1979/01/06;racn-street; 54;ID;IDAHO + 122;jsxhg-name;yoo-firstname; 13440;ulf-city;1982/06/28;krry-street; 55;NY;NEW YORK + 123;ugbci-name;vht-firstname; 17100;ffc-city;1962/10/16;ixha-street; 99;VT;VERMONT + 124;hgtkz-name;xgg-firstname; 17500;qck-city;1978/07/17;xkez-street; 60;AK;ALASKA + 125;ejdoc-name;ovv-firstname; 14920;ocs-city;1954/11/15;bwhd-street; 169;FL;FLORIDA + 126;zoucr-name;ivo-firstname; 14040;grt-city;1990/04/29;qhox-street; 90;WI;WISCONSIN + 127;asofx-name;yzv-firstname; 19600;ixo-city;1982/07/09;slmy-street; 198;MO;MISSOURI + 128;tgowi-name;zvm-firstname; 17560;avv-city;1986/11/04;qnyp-street; 83;AS;AMERICAN SAMOA + 129;toeur-name;ydp-firstname; 17260;dpz-city;1962/11/02;atag-street; 60;IN;INDIANA + 130;eohex-name;vfp-firstname; 17000;gzc-city;1970/01/05;cvlk-street; 188;UT;UTAH + 131;mahci-name;cwt-firstname; 18240;wut-city;1982/08/02;zyse-street; 147;OR;OREGON + 132;hzetq-name;kif-firstname; 14280;aep-city;1990/11/26;rrsr-street; 118;NE;NEBRASKA + 133;tjrgp-name;vle-firstname; 15620;sdv-city;1962/12/08;zdyx-street; 88;WV;WEST VIRGINIA + 134;japyi-name;jkm-firstname; 14960;jeo-city;1958/03/01;bsxv-street; 86;TX;TEXAS + 135;eqley-name;ttv-firstname; 12740;trs-city;1974/09/16;ibff-street; 187;CA;CALIFORNIA + 136;dbvkz-name;efr-firstname; 13700;ujo-city;1958/05/14;louh-street; 22;MP;NORTHERN MARIANA ISLANDS + 137;mpgcz-name;ysk-firstname; 11860;eva-city;1978/08/23;nedx-street; 101;MT;MONTANA + 138;bntsw-name;osn-firstname; 15700;mmv-city;1966/07/28;loqs-street; 34;WY;WYOMING + 139;yfcbv-name;ing-firstname; 10300;ddb-city;1966/04/12;amuj-street; 32;SD;SOUTH DAKOTA + 140;ddwqy-name;rxo-firstname; 18720;nsh-city;1974/06/22;rugn-street; 105;LA;LOUISIANA + 141;zuxwq-name;xha-firstname; 12240;jii-city;1974/04/22;kawh-street; 97;NJ;NEW JERSEY + 142;aarej-name;dfg-firstname; 14680;exu-city;1958/04/17;adua-street; 11;NE;NEBRASKA + 143;iezfw-name;ufb-firstname; 18800;fyv-city;1970/06/05;yvao-street; 53;HI;HAWAII + 144;vvmzr-name;bud-firstname; 15120;ggo-city;1966/07/24;ozcj-street; 127;MT;MONTANA + 145;bknbv-name;qrd-firstname; 11500;mth-city;1970/04/16;ijle-street; 143;NH;NEW HAMPSHIRE + 146;bwyxl-name;fdq-firstname; 13160;ngn-city;1954/07/05;nkco-street; 120;DE;DELAWARE + 147;lkkwb-name;yqh-firstname; 19580;pwn-city;1954/10/16;rgdl-street; 185;MN;MINNESOTA + 148;uokzd-name;aco-firstname; 13940;wyf-city;1966/02/07;lbhd-street; 23;NH;NEW HAMPSHIRE + 149;tdmol-name;hkb-firstname; 11960;wbi-city;1970/06/03;wboh-street; 59;ND;NORTH DAKOTA + 150;erulk-name;xcd-firstname; 11420;kzt-city;1990/02/07;bmcb-street; 160;DC;DISTRICT OF COLUMBIA + 151;atrip-name;mlq-firstname; 14440;agk-city;1986/11/08;qhdv-street; 29;IN;INDIANA + 152;atiir-name;brc-firstname; 11380;sas-city;1958/10/20;dwyv-street; 100;AR;ARKANSAS + 153;cvygb-name;kdu-firstname; 17300;etl-city;1954/11/13;bdxo-street; 43;MA;MASSACHUSETTS + 154;jeyeq-name;yjl-firstname; 12740;jgr-city;1978/06/21;aavd-street; 61;NY;NEW YORK + 155;wojgm-name;xdk-firstname; 13340;meq-city;1982/10/20;ttix-street; 61;MT;MONTANA + 156;oktge-name;taf-firstname; 11200;ibx-city;1990/09/05;clbk-street; 70;UT;UTAH + 157;cdrrm-name;dmu-firstname; 11980;bqa-city;1962/06/18;owlk-street; 26;NY;NEW YORK + 158;jyqvl-name;rht-firstname; 11120;qrk-city;1982/04/20;qbrn-street; 55;WY;WYOMING + 159;spfxh-name;oqv-firstname; 14740;gyh-city;1970/07/08;oiin-street; 59;NC;NORTH CAROLINA + 160;sczwy-name;mhg-firstname; 17860;izz-city;1970/08/25;xehg-street; 2;NJ;NEW JERSEY + 161;lklcm-name;rcy-firstname; 11960;ycf-city;1982/07/04;path-street; 179;NJ;NEW JERSEY + 162;jcluy-name;tlk-firstname; 10380;lsi-city;1970/03/17;ugqr-street; 138;NJ;NEW JERSEY + 163;qqdvp-name;hsh-firstname; 18240;bqf-city;1982/01/01;nupe-street; 153;LA;LOUISIANA + 164;rxlox-name;uoi-firstname; 15600;uvd-city;1954/04/03;gjmv-street; 197;MA;MASSACHUSETTS + 165;kjypq-name;wgt-firstname; 14060;yrs-city;1978/06/09;ijks-street; 144;CO;COLORADO + 166;zegdj-name;fpi-firstname; 13380;znp-city;1978/04/26;pdlh-street; 187;KY;KENTUCKY + 167;ihkkq-name;gtk-firstname; 15740;qbg-city;1970/06/18;odsg-street; 95;CO;COLORADO + 168;yhnuk-name;uhh-firstname; 16720;hoo-city;1978/06/20;vrcy-street; 186;KS;KANSAS + 169;ftpvt-name;ufk-firstname; 13600;wat-city;1954/10/16;nxax-street; 112;OR;OREGON + 170;xoiyz-name;xqq-firstname; 14560;kea-city;1986/08/10;bivl-street; 177;MN;MINNESOTA + 171;wfeuq-name;qec-firstname; 16540;obq-city;1950/11/17;keyf-street; 108;UT;UTAH + 172;pfrmg-name;tyi-firstname; 15360;tjx-city;1979/01/30;lyhr-street; 78;NE;NEBRASKA + 173;najqw-name;ldk-firstname; 10220;bci-city;1982/04/01;qxuf-street; 84;MS;MISSISSIPPI + 174;qbqrg-name;zyo-firstname; 13420;cdh-city;1958/06/13;gqst-street; 167;WY;WYOMING + 175;lnaxv-name;zwt-firstname; 14740;lok-city;1962/10/06;mmdu-street; 149;KY;KENTUCKY + 176;tpgpm-name;qie-firstname; 14960;opy-city;1958/07/14;uxfv-street; 158;SC;SOUTH CAROLINA + 177;nltvw-name;ahc-firstname; 19520;uxf-city;1958/03/16;fwsy-street; 131;CA;CALIFORNIA + 178;ujfpc-name;cwd-firstname; 13800;gki-city;1974/10/10;sgiq-street; 12;FL;FLORIDA + 179;pehcm-name;mah-firstname; 15940;azs-city;1970/05/07;hvvk-street; 9;PR;PUERTO RICO + 180;phlqr-name;qog-firstname; 12160;qvt-city;1966/09/11;isol-street; 155;AZ;ARIZONA + 181;bxerk-name;kxv-firstname; 14180;sek-city;1982/02/18;ctwu-street; 84;CA;CALIFORNIA + 182;nmlqw-name;oyf-firstname; 12640;tmv-city;1962/02/26;eqss-street; 141;NE;NEBRASKA + 183;kobcl-name;pht-firstname; 15820;nky-city;1978/05/14;vfnd-street; 176;PR;PUERTO RICO + 184;lvbqi-name;juh-firstname; 12780;rst-city;1958/12/18;wwko-street; 22;MP;NORTHERN MARIANA ISLANDS + 185;yqqmt-name;zrg-firstname; 12780;hxs-city;1954/08/12;mdxh-street; 190;MS;MISSISSIPPI + 186;osbyt-name;qtk-firstname; 14900;ltd-city;1990/06/21;quqn-street; 59;MO;MISSOURI + 187;vibab-name;vgy-firstname; 19600;jxa-city;1958/09/20;czps-street; 137;AR;ARKANSAS + 188;vrnml-name;qmd-firstname; 15860;mxe-city;1966/07/23;ybfc-street; 148;DE;DELAWARE + 189;thimt-name;ige-firstname; 12900;dqn-city;1966/05/07;bccw-street; 187;OK;OKLAHOMA + 190;jdzou-name;qnd-firstname; 17600;fzi-city;1958/06/12;ewtx-street; 174;IN;INDIANA + 191;bsvvw-name;hfa-firstname; 14180;kmn-city;1974/09/19;zvdw-street; 13;UT;UTAH + 192;iwbao-name;qur-firstname; 19500;jlk-city;1982/08/08;kllj-street; 113;WA;WASHINGTON + 193;xpxla-name;yzv-firstname; 19020;eze-city;1954/04/22;taku-street; 105;AS;AMERICAN SAMOA + 194;gqugh-name;sdy-firstname; 14360;pwi-city;1974/03/11;qybh-street; 95;KY;KENTUCKY + 195;bueoc-name;sfx-firstname; 10560;xhn-city;1970/08/29;zfin-street; 48;NC;NORTH CAROLINA + 196;fyrln-name;fay-firstname; 10820;qtd-city;1974/11/04;yrtc-street; 120;MH;MARSHALL ISLANDS + 197;zuhli-name;qwr-firstname; 19800;nqp-city;1970/01/21;mdew-street; 8;VA;VIRGINIA + 198;rwplk-name;jkr-firstname; 18080;khf-city;1978/02/28;ihkv-street; 134;MT;MONTANA + 199;ssbzy-name;azn-firstname; 11440;ire-city;1954/11/16;sjou-street; 55;CO;COLORADO + 200;zbhue-name;ces-firstname; 19840;ybc-city;1974/07/17;ktsw-street; 94;ND;NORTH DAKOTA + 201;tcpnt-name;tgk-firstname; 19580;nox-city;1990/02/24;rmst-street; 59;MS;MISSISSIPPI + 202;avrpe-name;aaz-firstname; 14000;anm-city;1950/09/02;ddjz-street; 197;FL;FLORIDA + 203;qemau-name;lbl-firstname; 15620;jkx-city;1962/07/23;kxdn-street; 38;PA;PENNSYLVANIA + 204;xzeqe-name;bjx-firstname; 12960;qiv-city;1958/09/07;yohx-street; 22;VA;VIRGINIA + 205;ufbqh-name;dcm-firstname; 17720;tch-city;1978/10/16;sqis-street; 119;NM;NEW MEXICO + 206;cfwje-name;kng-firstname; 15980;hmf-city;1974/09/23;timl-street; 105;NV;NEVADA + 207;dpswi-name;lzu-firstname; 11020;mby-city;1962/10/14;stnj-street; 143;UT;UTAH + 208;padrh-name;yvj-firstname; 17680;pqc-city;1986/11/28;xmxq-street; 81;GU;GUAM + 209;blcun-name;erh-firstname; 16200;sgc-city;1950/10/10;sqkp-street; 29;PA;PENNSYLVANIA + 210;jxuox-name;ztl-firstname; 13140;hox-city;1962/08/12;vxgj-street; 83;KS;KANSAS + 211;bcfua-name;urk-firstname; 11540;mhn-city;1982/10/09;poor-street; 21;VI;VIRGIN ISLANDS + 212;nmccn-name;nlv-firstname; 11780;dec-city;1974/07/05;txyt-street; 125;FL;FLORIDA + 213;cwcfl-name;nye-firstname; 10620;ciu-city;1974/06/14;dumh-street; 124;TX;TEXAS + 214;bewhj-name;mcq-firstname; 16040;vir-city;1951/01/31;uhse-street; 78;MA;MASSACHUSETTS + 215;owbls-name;mcq-firstname; 12940;zjk-city;1962/08/21;plgy-street; 185;NJ;NEW JERSEY + 216;tjwtx-name;uur-firstname; 13400;jqa-city;1962/05/15;hhzi-street; 134;KS;KANSAS + 217;aynmw-name;obp-firstname; 13820;hbu-city;1974/12/09;lfeb-street; 28;NV;NEVADA + 218;aivbc-name;fkc-firstname; 14980;mew-city;1958/05/19;rxqg-street; 41;AK;ALASKA + 219;nkeha-name;ddi-firstname; 17680;wzu-city;1986/03/04;xtik-street; 11;RI;RHODE ISLAND + 220;hyyqm-name;vac-firstname; 17600;gph-city;1954/04/28;rjxi-street; 22;NY;NEW YORK + 221;sifar-name;yth-firstname; 12840;kbe-city;1982/11/18;mnje-street; 5;NY;NEW YORK + 222;bxlyk-name;pla-firstname; 15740;zpb-city;1990/12/07;viys-street; 171;HI;HAWAII + 223;ifcwk-name;yfu-firstname; 10000;itv-city;1982/06/16;iuya-street; 77;SD;SOUTH DAKOTA + 224;fherw-name;acw-firstname; 13000;dxg-city;1970/09/09;zscv-street; 45;VT;VERMONT + 225;cvbfh-name;tbs-firstname; 13160;znt-city;1958/07/20;exfl-street; 171;LA;LOUISIANA + 226;sxpuq-name;qdu-firstname; 13000;lhm-city;1971/01/08;yooq-street; 80;VT;VERMONT + 227;xawor-name;glz-firstname; 18160;dxx-city;1954/12/08;fjnf-street; 130;FM;FEDERATED STATES OF MICRONESIA + 228;dwpda-name;dtg-firstname; 15380;zyz-city;1974/04/21;gozg-street; 96;MT;MONTANA + 229;airyv-name;oue-firstname; 16900;gbm-city;1986/07/14;xfte-street; 45;MP;NORTHERN MARIANA ISLANDS + 230;omfog-name;zhv-firstname; 17020;lep-city;1970/03/21;trww-street; 128;LA;LOUISIANA + 231;ddgah-name;ost-firstname; 13580;ojl-city;1958/03/07;gnln-street; 155;KY;KENTUCKY + 232;feggq-name;cro-firstname; 18780;wtj-city;1966/10/01;jesi-street; 63;NM;NEW MEXICO + 233;ahxvq-name;nes-firstname; 11660;niu-city;1950/06/06;upyk-street; 185;WA;WASHINGTON + 234;gjlqf-name;mvv-firstname; 19620;roc-city;1974/05/01;tsqu-street; 19;MI;MICHIGAN + 235;xbcip-name;vyn-firstname; 10560;nru-city;1986/12/06;qxfi-street; 114;WI;WISCONSIN + 236;cdwuj-name;sks-firstname; 12560;typ-city;1954/01/31;fkwb-street; 128;DC;DISTRICT OF COLUMBIA + 237;bsekx-name;wbw-firstname; 14280;twm-city;1962/09/11;bxui-street; 174;NM;NEW MEXICO + 238;foppd-name;zlw-firstname; 13580;hmg-city;1974/04/29;yiwk-street; 68;MN;MINNESOTA + 239;brtej-name;cqi-firstname; 11000;elz-city;1982/10/16;uauh-street; 23;PA;PENNSYLVANIA + 240;cpklp-name;tps-firstname; 11440;nsm-city;1950/10/28;cmjv-street; 139;PR;PUERTO RICO + 241;opbzn-name;bxz-firstname; 12860;jnq-city;1966/05/08;nkuq-street; 35;MP;NORTHERN MARIANA ISLANDS + 242;kxkmx-name;ziy-firstname; 17460;wqq-city;1974/11/05;gnha-street; 192;WV;WEST VIRGINIA + 243;lxpbu-name;jph-firstname; 19500;fpa-city;1954/10/01;gdls-street; 163;MI;MICHIGAN + 244;erhwd-name;wvu-firstname; 11880;iza-city;1962/08/03;ucsh-street; 75;undefined;undefined + 245;tjuxy-name;jzf-firstname; 10580;nyq-city;1970/04/30;gbes-street; 189;AR;ARKANSAS + 246;piocq-name;skz-firstname; 14600;xuq-city;1978/07/12;inae-street; 27;ME;MAINE + 247;bqjty-name;ybj-firstname; 13040;jqu-city;1954/11/30;ugcn-street; 159;AL;ALABAMA + 248;xexrx-name;fpu-firstname; 19720;ckc-city;1974/06/30;zpez-street; 46;IN;INDIANA + 249;auzgu-name;dam-firstname; 18460;mih-city;1962/07/28;augp-street; 112;MD;MARYLAND + 250;xupoe-name;fdb-firstname; 13440;llr-city;1986/10/01;forq-street; 185;IL;ILLINOIS + 251;sgely-name;pzz-firstname; 15920;jya-city;1950/02/02;kypg-street; 147;DC;DISTRICT OF COLUMBIA + 252;onini-name;zts-firstname; 18060;avs-city;1974/12/02;kxjn-street; 85;TX;TEXAS + 253;tyflk-name;htl-firstname; 17560;bhd-city;1962/06/06;xquf-street; 126;IN;INDIANA + 254;chbez-name;zkj-firstname; 17000;goh-city;1974/03/02;rkui-street; 13;AR;ARKANSAS + 255;zmmhg-name;rqb-firstname; 11340;egt-city;1970/02/06;pwjj-street; 6;MH;MARSHALL ISLANDS + 256;gutfo-name;vki-firstname; 18860;xdv-city;1986/07/11;iwkf-street; 14;DC;DISTRICT OF COLUMBIA + 257;eogwr-name;hnt-firstname; 19840;nht-city;1990/05/02;kljr-street; 18;VT;VERMONT + 258;ibupi-name;ygc-firstname; 18580;tvk-city;1978/08/20;xphm-street; 123;CO;COLORADO + 259;wqpaq-name;uwc-firstname; 10780;ygl-city;1958/03/20;ncta-street; 87;GU;GUAM + 260;swcms-name;ljb-firstname; 13560;pvt-city;1954/12/13;ovsf-street; 176;MD;MARYLAND + 261;xwxmt-name;qpq-firstname; 13380;fzc-city;1978/10/14;ivwb-street; 17;AZ;ARIZONA + 262;drxej-name;msd-firstname; 12720;ure-city;1986/02/15;nrqa-street; 30;MT;MONTANA + 263;scgpq-name;wgg-firstname; 18740;xtx-city;1990/05/09;vxwl-street; 29;AK;ALASKA + 264;qchcm-name;fbc-firstname; 14480;ymf-city;1978/03/21;cfqc-street; 63;ND;NORTH DAKOTA + 265;cbpxm-name;clb-firstname; 16900;etx-city;1974/03/29;uzyw-street; 175;OK;OKLAHOMA + 266;clksy-name;mls-firstname; 13560;qhs-city;1958/04/07;trwx-street; 47;MO;MISSOURI + 267;qwagv-name;hil-firstname; 18240;atx-city;1954/01/30;glaf-street; 57;MT;MONTANA + 268;grkjm-name;qwy-firstname; 15900;jvu-city;1970/02/26;rhdn-street; 172;ME;MAINE + 269;gkiis-name;xhp-firstname; 14440;bgh-city;1954/01/02;btrx-street; 53;FL;FLORIDA + 270;nscdn-name;lfn-firstname; 19900;eph-city;1958/09/29;nqao-street; 171;KS;KANSAS + 271;ulrrc-name;ncb-firstname; 10320;cao-city;1986/10/15;lkrm-street; 125;GA;GEORGIA + 272;qxjcn-name;wpy-firstname; 11260;rew-city;1954/12/19;tldv-street; 115;CA;CALIFORNIA + 273;thyyj-name;htd-firstname; 19100;tae-city;1982/03/04;wbqv-street; 174;PR;PUERTO RICO + 274;uigiq-name;lmx-firstname; 19320;bkr-city;1950/04/07;foio-street; 104;OK;OKLAHOMA + 275;iinbr-name;cfg-firstname; 12100;qwv-city;1986/01/12;ervo-street; 87;MN;MINNESOTA + 276;kipbw-name;fda-firstname; 16300;mlu-city;1970/03/26;yjpd-street; 15;MN;MINNESOTA + 277;cfrgd-name;dui-firstname; 11320;jax-city;1990/04/14;zaik-street; 157;ME;MAINE + 278;duaza-name;xsx-firstname; 11120;kkg-city;1958/02/05;bply-street; 185;VI;VIRGIN ISLANDS + 279;zzhqx-name;vlb-firstname; 18380;vyb-city;1950/12/11;iugj-street; 159;HI;HAWAII + 280;kuryf-name;kib-firstname; 13140;tum-city;1954/05/06;clkw-street; 31;CO;COLORADO + 281;daljt-name;ycr-firstname; 10840;ckw-city;1982/10/29;umof-street; 52;CO;COLORADO + 282;xhssg-name;djc-firstname; 17840;gvj-city;1954/12/12;zgmw-street; 35;WV;WEST VIRGINIA + 283;qqscv-name;eiu-firstname; 15260;daj-city;1986/01/30;qprc-street; 158;MH;MARSHALL ISLANDS + 284;tehbb-name;czj-firstname; 14180;xnh-city;1958/03/06;lfji-street; 48;UT;UTAH + 285;acdsd-name;yiu-firstname; 12220;buk-city;1958/02/15;ovcj-street; 70;ME;MAINE + 286;derpl-name;buv-firstname; 16000;fha-city;1970/09/02;vfay-street; 57;WA;WASHINGTON + 287;lobqd-name;qxn-firstname; 15060;ycp-city;1966/12/24;axxb-street; 38;PA;PENNSYLVANIA + 288;owxja-name;okb-firstname; 15640;lad-city;1982/05/24;wnka-street; 101;MN;MINNESOTA + 289;zohkw-name;ypo-firstname; 15460;wtu-city;1978/08/10;jhco-street; 21;VA;VIRGINIA + 290;nmrar-name;aqm-firstname; 18360;lcn-city;1963/01/26;zxeo-street; 143;WV;WEST VIRGINIA + 291;tndrh-name;ael-firstname; 16360;cyb-city;1958/04/10;cmlg-street; 78;ND;NORTH DAKOTA + 292;kbpkv-name;yck-firstname; 19400;oka-city;1974/11/02;kpmc-street; 10;CO;COLORADO + 293;uabcl-name;zms-firstname; 15940;tzb-city;1970/08/06;ezzs-street; 11;VT;VERMONT + 294;lancp-name;zbk-firstname; 11560;vny-city;1978/07/21;tzys-street; 182;LA;LOUISIANA + 295;wagzv-name;hcp-firstname; 13760;kik-city;1974/11/01;gpuw-street; 151;NH;NEW HAMPSHIRE + 296;qrenr-name;egp-firstname; 16920;zwq-city;1966/03/19;fbwi-street; 102;FL;FLORIDA + 297;amjep-name;mds-firstname; 16700;fvb-city;1978/05/07;peau-street; 167;WV;WEST VIRGINIA + 298;dzppv-name;qav-firstname; 16680;wbc-city;1970/04/15;dzyp-street; 149;VI;VIRGIN ISLANDS + 299;qrlwz-name;hvk-firstname; 14640;qyl-city;1954/03/07;gvsc-street; 32;NC;NORTH CAROLINA + 300;lracx-name;dmp-firstname; 13800;slo-city;1970/09/21;jspb-street; 106;WA;WASHINGTON + 301;jhlao-name;txt-firstname; 12020;jam-city;1962/06/26;bcky-street; 19;WV;WEST VIRGINIA + 302;aocxq-name;mzv-firstname; 17140;dgx-city;1954/08/24;maoi-street; 1;HI;HAWAII + 303;dvdbu-name;fdf-firstname; 10980;goa-city;1982/04/10;onik-street; 109;NM;NEW MEXICO + 304;cqapx-name;skq-firstname; 11080;akw-city;1958/09/02;xakx-street; 14;CA;CALIFORNIA + 305;crmlf-name;djf-firstname; 16100;mle-city;1974/10/10;udrl-street; 96;VT;VERMONT + 306;iujcn-name;tat-firstname; 17660;jnf-city;1966/03/31;inoh-street; 104;TX;TEXAS + 307;aetvh-name;spn-firstname; 14960;wxf-city;1954/06/30;mmxe-street; 78;PA;PENNSYLVANIA + 308;ibfdt-name;sfu-firstname; 11120;kqf-city;1954/06/22;xbwg-street; 132;CA;CALIFORNIA + 309;ccatv-name;aeb-firstname; 10940;qoi-city;1982/08/11;bsrg-street; 29;UT;UTAH + 310;udrjw-name;agc-firstname; 17100;aep-city;1974/04/23;bsju-street; 59;PR;PUERTO RICO + 311;crjcx-name;nbb-firstname; 14820;xtp-city;1958/11/08;fajh-street; 95;WY;WYOMING + 312;qbcrw-name;mef-firstname; 14220;mwa-city;1982/11/13;keyy-street; 97;NE;NEBRASKA + 313;wxvpi-name;dym-firstname; 10220;ncp-city;1954/09/28;qgrg-street; 25;undefined;undefined + 314;bandc-name;hzf-firstname; 18700;eoh-city;1990/11/05;qphi-street; 177;IL;ILLINOIS + 315;afatf-name;eii-firstname; 15480;lsm-city;1982/10/11;nzjq-street; 96;VI;VIRGIN ISLANDS + 316;trxge-name;vhe-firstname; 18260;ccm-city;1958/03/09;ducm-street; 1;NH;NEW HAMPSHIRE + 317;kdygt-name;tgc-firstname; 12500;bqo-city;1978/06/20;rqgx-street; 90;DC;DISTRICT OF COLUMBIA + 318;angdt-name;lvt-firstname; 13960;tbr-city;1974/07/14;wfqj-street; 196;GU;GUAM + 319;hqhqe-name;nxd-firstname; 14860;yhi-city;1954/04/23;wkpi-street; 23;MP;NORTHERN MARIANA ISLANDS + 320;pwwep-name;qtw-firstname; 15160;utn-city;1958/06/08;sheh-street; 176;FL;FLORIDA + 321;vkken-name;wrw-firstname; 16760;jhm-city;1974/12/13;czmf-street; 183;AR;ARKANSAS + 322;zbbmz-name;ipe-firstname; 14340;mco-city;1970/04/25;ymou-street; 2;WY;WYOMING + 323;eyzfr-name;xeb-firstname; 10440;qsa-city;1990/04/15;iukl-street; 135;AZ;ARIZONA + 324;tfpxy-name;yzh-firstname; 14080;wbc-city;1970/02/12;ojfc-street; 150;MP;NORTHERN MARIANA ISLANDS + 325;tphou-name;xac-firstname; 15940;ncy-city;1962/09/22;vcxl-street; 90;AL;ALABAMA + 326;bfscx-name;yih-firstname; 18900;bxa-city;1962/10/12;qsww-street; 137;VT;VERMONT + 327;ussbw-name;gbb-firstname; 15600;wtg-city;1978/07/29;vqcq-street; 10;KY;KENTUCKY + 328;zkoqj-name;mqn-firstname; 13440;lux-city;1970/05/01;bokx-street; 106;VA;VIRGINIA + 329;zbqew-name;dbw-firstname; 14520;hbi-city;1971/01/20;syea-street; 192;CT;CONNECTICUT + 330;vmfuy-name;qge-firstname; 16660;vnk-city;1982/08/29;tlxy-street; 166;PW;PALAU + 331;wajgr-name;mnx-firstname; 17500;wiv-city;1954/07/10;ylug-street; 21;OR;OREGON + 332;lzcry-name;tzk-firstname; 18980;icz-city;1966/06/04;ldvu-street; 25;GU;GUAM + 333;rodpv-name;rix-firstname; 16540;tpf-city;1986/02/03;leur-street; 169;SD;SOUTH DAKOTA + 334;pxmhq-name;tyd-firstname; 11200;okj-city;1978/12/30;tauk-street; 94;ND;NORTH DAKOTA + 335;kansg-name;qzs-firstname; 18020;yhj-city;1982/08/27;ehie-street; 140;AR;ARKANSAS + 336;kpqff-name;bqs-firstname; 13780;tnp-city;1958/08/07;euhw-street; 182;SD;SOUTH DAKOTA + 337;akvdd-name;bxi-firstname; 15620;rbk-city;1962/03/16;fzdy-street; 125;WA;WASHINGTON + 338;nbazl-name;ikv-firstname; 13160;wci-city;1966/08/22;smzb-street; 136;SD;SOUTH DAKOTA + 339;zrowi-name;udo-firstname; 16460;hbe-city;1962/07/07;xgyd-street; 170;AZ;ARIZONA + 340;jfzjd-name;atn-firstname; 13040;yef-city;1974/09/26;dyjj-street; 127;WI;WISCONSIN + 341;prsbo-name;ibh-firstname; 19520;qro-city;1986/01/10;pemg-street; 183;SC;SOUTH CAROLINA + 342;dwfdn-name;kci-firstname; 14640;fze-city;1970/07/24;xxdz-street; 102;IA;IOWA + 343;cnupm-name;rjl-firstname; 14660;ewk-city;1966/07/02;wzjr-street; 107;PW;PALAU + 344;wbpor-name;fuf-firstname; 11300;xne-city;1978/02/14;tyzx-street; 2;AL;ALABAMA + 345;tjqky-name;mjf-firstname; 12560;qlo-city;1982/10/21;hzos-street; 121;NE;NEBRASKA + 346;yxqsn-name;ofx-firstname; 18680;ute-city;1974/02/27;wqka-street; 111;FL;FLORIDA + 347;wjwmv-name;hra-firstname; 14520;mvj-city;1954/10/03;chmz-street; 70;SD;SOUTH DAKOTA + 348;aipnn-name;paa-firstname; 14400;gyq-city;1970/04/27;tbpq-street; 84;undefined;undefined + 349;wplyl-name;zvh-firstname; 15820;vhw-city;1967/01/12;eesj-street; 110;undefined;undefined + 350;syoip-name;lbd-firstname; 13860;wfo-city;1966/03/17;lsvf-street; 47;VI;VIRGIN ISLANDS + 351;lvhdk-name;pzl-firstname; 14320;bab-city;1950/05/01;wktz-street; 134;AR;ARKANSAS + 352;jodya-name;apd-firstname; 14520;nfr-city;1986/12/14;jgzs-street; 193;KY;KENTUCKY + 353;ikunj-name;syx-firstname; 17320;cqd-city;1982/08/19;xwzj-street; 53;IN;INDIANA + 354;bnhud-name;uvd-firstname; 15360;ynw-city;1971/01/25;gens-street; 107;MT;MONTANA + 355;prkrf-name;ivm-firstname; 19220;kie-city;1966/11/14;boem-street; 59;HI;HAWAII + 356;ucywi-name;sjl-firstname; 11480;ish-city;1982/11/13;szck-street; 148;NE;NEBRASKA + 357;sgftd-name;ajp-firstname; 11760;jco-city;1958/03/05;djek-street; 196;UT;UTAH + 358;kohdq-name;phr-firstname; 13120;kpb-city;1978/07/26;csom-street; 17;PR;PUERTO RICO + 359;fgjoa-name;srp-firstname; 11480;onn-city;1990/05/26;duvo-street; 127;MS;MISSISSIPPI + 360;xgiyw-name;rcu-firstname; 15140;mti-city;1958/09/26;lego-street; 125;MA;MASSACHUSETTS + 361;kgcui-name;grq-firstname; 12640;tjn-city;1986/02/08;ntoq-street; 6;CT;CONNECTICUT + 362;ljyxw-name;lho-firstname; 19100;hmd-city;1962/03/09;mqus-street; 12;CT;CONNECTICUT + 363;vnvwy-name;vmv-firstname; 14360;cpu-city;1966/04/20;cfts-street; 16;MI;MICHIGAN + 364;gedwu-name;ocl-firstname; 11760;vaf-city;1970/04/22;exot-street; 121;MT;MONTANA + 365;enknw-name;vjk-firstname; 11100;bge-city;1954/07/06;inhb-street; 95;AS;AMERICAN SAMOA + 366;qmpom-name;tmp-firstname; 12260;ymn-city;1986/07/02;psdj-street; 71;MI;MICHIGAN + 367;jrbwj-name;imr-firstname; 11080;qsx-city;1966/11/08;msys-street; 80;ID;IDAHO + 368;dzjpp-name;cwl-firstname; 15720;ipm-city;1958/07/13;acsb-street; 87;IL;ILLINOIS + 369;qdwdn-name;dtc-firstname; 12740;qab-city;1986/11/29;besk-street; 80;DC;DISTRICT OF COLUMBIA + 370;gbeyp-name;lzl-firstname; 19740;ljz-city;1974/10/01;zwga-street; 52;RI;RHODE ISLAND + 371;kuwva-name;noq-firstname; 17500;xia-city;1950/03/18;omzn-street; 164;KS;KANSAS + 372;jjhsm-name;cdc-firstname; 13020;xli-city;1986/06/10;nups-street; 38;VA;VIRGINIA + 373;xyupl-name;fyd-firstname; 16100;fqd-city;1971/01/05;icjo-street; 28;NE;NEBRASKA + 374;ueipj-name;meb-firstname; 19880;nsk-city;1974/08/15;aqwr-street; 14;LA;LOUISIANA + 375;vdmif-name;fat-firstname; 19120;dud-city;1974/07/01;krgw-street; 178;MS;MISSISSIPPI + 376;oxgdf-name;apn-firstname; 11400;dji-city;1962/04/02;ttnt-street; 13;HI;HAWAII + 377;xgxyc-name;jrn-firstname; 19400;bfg-city;1950/12/30;jzgy-street; 43;WY;WYOMING + 378;bczfk-name;bfu-firstname; 16920;qxp-city;1974/05/26;seja-street; 178;HI;HAWAII + 379;pppqe-name;kwo-firstname; 15260;geb-city;1986/04/06;okvv-street; 11;MN;MINNESOTA + 380;opzyy-name;mfk-firstname; 14960;nlo-city;1962/03/03;dezo-street; 106;AS;AMERICAN SAMOA + 381;xmnsm-name;ckj-firstname; 10400;fnr-city;1950/10/21;uhme-street; 154;NC;NORTH CAROLINA + 382;moqtn-name;zgw-firstname; 12920;pqk-city;1950/10/28;suvi-street; 102;WY;WYOMING + 383;byapu-name;pix-firstname; 10900;gik-city;1982/09/04;ntiq-street; 45;VI;VIRGIN ISLANDS + 384;zuhdb-name;gbj-firstname; 18760;vkk-city;1978/06/17;vnem-street; 62;TX;TEXAS + 385;gkjpo-name;qwq-firstname; 12380;ame-city;1982/12/01;ndvp-street; 94;VA;VIRGINIA + 386;yefwf-name;aev-firstname; 13580;mor-city;1962/07/09;ldcg-street; 91;AL;ALABAMA + 387;twgsp-name;mwx-firstname; 15840;bbp-city;1970/05/12;hecl-street; 137;WY;WYOMING + 388;zatdb-name;tes-firstname; 17080;yga-city;1954/11/05;dfwu-street; 58;undefined;undefined + 389;qxdyx-name;tum-firstname; 18100;mqw-city;1958/10/21;gndl-street; 156;FL;FLORIDA + 390;ncpki-name;wbm-firstname; 13860;cuo-city;1970/07/02;yqpa-street; 30;NE;NEBRASKA + 391;kyzsl-name;djj-firstname; 17400;zzd-city;1978/02/11;mvkp-street; 4;ME;MAINE + 392;jtksy-name;ayp-firstname; 10760;gui-city;1982/12/17;wohf-street; 175;DC;DISTRICT OF COLUMBIA + 393;grzby-name;oyz-firstname; 15140;bmz-city;1974/07/10;rrui-street; 60;NC;NORTH CAROLINA + 394;sgaut-name;xzd-firstname; 15260;dnb-city;1978/10/01;hcii-street; 169;RI;RHODE ISLAND + 395;ozkaq-name;brx-firstname; 11400;guw-city;1962/02/08;pswz-street; 194;NH;NEW HAMPSHIRE + 396;uqivg-name;map-firstname; 10880;lgr-city;1990/07/14;lxmi-street; 143;MN;MINNESOTA + 397;soqkg-name;jws-firstname; 17200;dss-city;1970/12/05;ppht-street; 187;UT;UTAH + 398;pdqdm-name;erh-firstname; 11860;obj-city;1986/04/03;aova-street; 121;CT;CONNECTICUT + 399;ogqrv-name;uyf-firstname; 16400;abs-city;1987/01/06;xwue-street; 114;AZ;ARIZONA + 400;ukcxw-name;ltd-firstname; 11760;bwi-city;1986/09/01;ddjt-street; 199;AR;ARKANSAS + 401;djcgr-name;tet-firstname; 19380;ure-city;1962/09/03;alzp-street; 169;SC;SOUTH CAROLINA + 402;ibbvs-name;cyv-firstname; 16060;gdh-city;1982/04/24;qrdz-street; 116;NV;NEVADA + 403;blmke-name;jtq-firstname; 17260;tls-city;1970/05/28;eylx-street; 171;NV;NEVADA + 404;qpatk-name;mtt-firstname; 13400;nzv-city;1970/03/25;exby-street; 93;WY;WYOMING + 405;mbngf-name;pqp-firstname; 10720;ann-city;1966/09/01;rkpu-street; 146;CT;CONNECTICUT + 406;rcydx-name;usf-firstname; 12880;jou-city;1982/02/20;gtqw-street; 186;MO;MISSOURI + 407;srszq-name;dtb-firstname; 14340;yrs-city;1978/03/16;wtan-street; 89;MN;MINNESOTA + 408;ezlfr-name;ebd-firstname; 19420;ctw-city;1986/03/22;zjyv-street; 44;VT;VERMONT + 409;bhotj-name;hzt-firstname; 16480;rbt-city;1978/06/11;mbsm-street; 157;WV;WEST VIRGINIA + 410;udzwf-name;kfd-firstname; 13440;maj-city;1986/06/06;kerz-street; 180;AZ;ARIZONA + 411;yflfv-name;zdl-firstname; 13300;zix-city;1982/05/20;ozcp-street; 153;MN;MINNESOTA + 412;wlyyq-name;kdz-firstname; 11400;ygb-city;1966/11/07;zekv-street; 111;AS;AMERICAN SAMOA + 413;wonzh-name;eeb-firstname; 19920;qnt-city;1982/09/20;fxob-street; 95;LA;LOUISIANA + 414;eiwzh-name;hpm-firstname; 15860;bfo-city;1967/01/30;tkpg-street; 182;VA;VIRGINIA + 415;mbqgk-name;jsm-firstname; 15040;dai-city;1954/06/12;jqdh-street; 65;NE;NEBRASKA + 416;jhigj-name;qyn-firstname; 18360;ntm-city;1974/05/15;eghd-street; 188;ME;MAINE + 417;llytl-name;jqe-firstname; 11280;kkp-city;1974/04/03;oudg-street; 69;DE;DELAWARE + 418;urhgl-name;iji-firstname; 13760;lhf-city;1962/12/02;oywx-street; 6;SC;SOUTH CAROLINA + 419;uflrm-name;hef-firstname; 15040;usl-city;1974/11/06;qvgi-street; 103;OK;OKLAHOMA + 420;gbfui-name;goz-firstname; 14940;edu-city;1986/02/25;gkqy-street; 26;KS;KANSAS + 421;nysbp-name;fro-firstname; 18420;wqt-city;1958/12/19;vpoc-street; 89;GA;GEORGIA + 422;fmrwo-name;wlf-firstname; 17820;qwb-city;1962/05/03;stcz-street; 22;CA;CALIFORNIA + 423;racwd-name;kqr-firstname; 10180;xdr-city;1974/04/09;fqxz-street; 15;MO;MISSOURI + 424;jpopz-name;krm-firstname; 13420;fjx-city;1970/10/29;kyph-street; 54;NJ;NEW JERSEY + 425;fwdat-name;ppn-firstname; 15660;zqh-city;1986/06/24;zgfe-street; 61;SC;SOUTH CAROLINA + 426;orznz-name;hyy-firstname; 12020;lju-city;1982/07/01;xisy-street; 125;GU;GUAM + 427;spzxo-name;mpv-firstname; 13220;cbq-city;1962/05/09;qlqx-street; 53;OK;OKLAHOMA + 428;qxdra-name;ifp-firstname; 10480;nvu-city;1958/08/15;egsq-street; 133;MH;MARSHALL ISLANDS + 429;yhelu-name;jsc-firstname; 17200;clp-city;1954/12/01;vahx-street; 3;NV;NEVADA + 430;umvfv-name;mbe-firstname; 13600;knp-city;1954/07/16;oldf-street; 188;PA;PENNSYLVANIA + 431;mwcfe-name;xxi-firstname; 15080;chq-city;1974/12/22;kpsj-street; 163;NV;NEVADA + 432;wvkrp-name;dtr-firstname; 19840;pqv-city;1986/07/07;jnzd-street; 119;HI;HAWAII + 433;vqfja-name;kep-firstname; 19380;ydo-city;1966/11/11;gsfd-street; 12;AR;ARKANSAS + 434;ikbjr-name;ipd-firstname; 17920;uld-city;1990/03/03;tmbc-street; 49;KS;KANSAS + 435;hyjrs-name;jqp-firstname; 18820;vvm-city;1970/05/07;txye-street; 152;AR;ARKANSAS + 436;tuewv-name;lkd-firstname; 17060;slo-city;1970/07/06;htdm-street; 197;OK;OKLAHOMA + 437;muyws-name;iql-firstname; 19540;cwf-city;1962/04/27;knsx-street; 167;LA;LOUISIANA + 438;tgsga-name;lww-firstname; 17780;goh-city;1982/12/24;lzcl-street; 136;SD;SOUTH DAKOTA + 439;agsyj-name;fve-firstname; 19260;wgi-city;1970/03/07;aone-street; 48;WV;WEST VIRGINIA + 440;yrgqp-name;tni-firstname; 15820;mzp-city;1986/04/19;femc-street; 29;PR;PUERTO RICO + 441;ltjmw-name;cps-firstname; 13060;aeo-city;1954/07/17;bewv-street; 182;OK;OKLAHOMA + 442;gjkpl-name;tre-firstname; 16340;ndn-city;1962/08/27;ocld-street; 16;WV;WEST VIRGINIA + 443;ryvgz-name;bhe-firstname; 14960;lcg-city;1978/08/09;bxwa-street; 19;NM;NEW MEXICO + 444;zgwwi-name;umb-firstname; 11840;llj-city;1958/07/06;uiww-street; 115;MP;NORTHERN MARIANA ISLANDS + 445;qaczz-name;qng-firstname; 10900;umr-city;1970/01/17;mlsy-street; 6;VA;VIRGINIA + 446;xcruf-name;vbp-firstname; 17840;vzl-city;1982/06/17;oatg-street; 110;MN;MINNESOTA + 447;egqdv-name;boy-firstname; 18360;gfw-city;1958/10/07;qaoh-street; 104;AS;AMERICAN SAMOA + 448;digvt-name;fid-firstname; 11180;iyw-city;1958/03/13;pooo-street; 119;NV;NEVADA + 449;gxavv-name;bal-firstname; 17020;cra-city;1966/02/01;nchf-street; 122;CO;COLORADO + 450;tiwoz-name;vwo-firstname; 19340;oja-city;1982/07/16;bnsy-street; 149;MA;MASSACHUSETTS + 451;jemtu-name;gnk-firstname; 11180;gbb-city;1954/03/01;pmbh-street; 87;NC;NORTH CAROLINA + 452;rhdjl-name;qaf-firstname; 16500;nqr-city;1974/03/28;vmrd-street; 75;DE;DELAWARE + 453;qdkbn-name;cpl-firstname; 17460;ugb-city;1987/01/29;cwoa-street; 184;ME;MAINE + 454;tuhon-name;bbg-firstname; 10500;cer-city;1958/03/02;ttst-street; 184;WA;WASHINGTON + 455;wluxg-name;xpv-firstname; 18240;axq-city;1958/07/28;useg-street; 140;NY;NEW YORK + 456;ojkgh-name;tzq-firstname; 16000;guv-city;1990/11/25;hagy-street; 198;MN;MINNESOTA + 457;arxsx-name;ana-firstname; 18620;oxx-city;1986/04/22;hkdb-street; 28;SD;SOUTH DAKOTA + 458;ujpiw-name;bty-firstname; 14000;kiy-city;1978/08/10;bmgt-street; 176;PW;PALAU + 459;sufuk-name;izq-firstname; 17740;kfy-city;1986/10/29;xxcz-street; 26;MD;MARYLAND + 460;nedie-name;ajz-firstname; 11820;fnf-city;1970/04/08;ccnd-street; 108;MT;MONTANA + 461;vbfwv-name;anp-firstname; 19880;qag-city;1962/11/17;tbcc-street; 23;NJ;NEW JERSEY + 462;bmrzz-name;yfe-firstname; 11300;rgi-city;1970/10/01;xbjp-street; 26;FM;FEDERATED STATES OF MICRONESIA + 463;jewvw-name;ymh-firstname; 13100;kcv-city;1982/03/01;cjbt-street; 4;KY;KENTUCKY + 464;kxxpm-name;has-firstname; 18500;hlp-city;1954/04/03;qsmq-street; 77;MD;MARYLAND + 465;qkjxa-name;gdq-firstname; 12240;qtz-city;1954/11/25;mevz-street; 12;NM;NEW MEXICO + 466;vmdwq-name;vjm-firstname; 19980;rmz-city;1986/09/09;ifxg-street; 139;NC;NORTH CAROLINA + 467;ssgil-name;lkd-firstname; 14220;ndd-city;1963/01/19;rjln-street; 195;MD;MARYLAND + 468;szbhe-name;pwi-firstname; 10100;iij-city;1966/04/09;mojp-street; 177;AK;ALASKA + 469;eswrp-name;lts-firstname; 10080;cuk-city;1966/03/29;plor-street; 139;VI;VIRGIN ISLANDS + 470;tioeh-name;qgc-firstname; 11800;zre-city;1954/06/05;owaq-street; 98;LA;LOUISIANA + 471;fkzpf-name;bse-firstname; 19040;cor-city;1962/06/21;aamy-street; 53;NY;NEW YORK + 472;kczhq-name;hde-firstname; 16380;siz-city;1986/12/15;rawc-street; 127;ND;NORTH DAKOTA + 473;gpwqf-name;pae-firstname; 12820;rga-city;1958/12/19;djsk-street; 131;WY;WYOMING + 474;yvgmq-name;hzp-firstname; 19020;ioc-city;1966/03/22;zdbt-street; 106;FM;FEDERATED STATES OF MICRONESIA + 475;abjqk-name;pdo-firstname; 13040;vnj-city;1962/09/08;kvwv-street; 145;OR;OREGON + 476;adppx-name;gmz-firstname; 16560;pah-city;1962/03/14;ynqs-street; 107;WY;WYOMING + 477;lxcrs-name;arg-firstname; 12160;med-city;1990/03/14;wlag-street; 141;MT;MONTANA + 478;mrkfp-name;jbm-firstname; 19760;dhu-city;1970/06/12;idan-street; 145;UT;UTAH + 479;zmkad-name;vns-firstname; 10080;aoe-city;1955/01/25;qgbd-street; 174;FL;FLORIDA + 480;vftgh-name;nxs-firstname; 11580;igp-city;1954/05/17;fief-street; 183;MT;MONTANA + 481;bnafy-name;geg-firstname; 11160;sfp-city;1974/04/26;tpnq-street; 194;DE;DELAWARE + 482;mwqpn-name;lbw-firstname; 17660;oot-city;1974/04/09;qxgk-street; 18;AK;ALASKA + 483;cijcf-name;uvd-firstname; 17940;ioy-city;1982/02/07;gfiz-street; 147;MN;MINNESOTA + 484;kwjhv-name;swd-firstname; 12400;pue-city;1970/12/10;sall-street; 104;MN;MINNESOTA + 485;mfdfy-name;hsy-firstname; 18260;bzl-city;1982/09/08;hsyc-street; 76;RI;RHODE ISLAND + 486;cdrmm-name;mxa-firstname; 11520;rie-city;1962/12/21;dxed-street; 112;LA;LOUISIANA + 487;flyur-name;mzm-firstname; 12260;wyi-city;1978/07/07;xqoj-street; 156;WV;WEST VIRGINIA + 488;hkwnf-name;obg-firstname; 14520;bib-city;1967/01/20;mvee-street; 115;undefined;undefined + 489;kawax-name;pyn-firstname; 10520;zjh-city;1966/09/01;fsmz-street; 87;DE;DELAWARE + 490;nnhzs-name;sfo-firstname; 19040;thn-city;1990/11/08;tzsb-street; 43;IN;INDIANA + 491;xivec-name;gzo-firstname; 16820;aha-city;1978/10/19;iolt-street; 36;HI;HAWAII + 492;hyiju-name;plw-firstname; 18620;zzu-city;1982/07/13;tydq-street; 112;NV;NEVADA + 493;leylb-name;fcv-firstname; 15720;biw-city;1958/09/18;bnpf-street; 52;MT;MONTANA + 494;pweci-name;hcu-firstname; 19020;fzb-city;1950/10/28;spdv-street; 131;WY;WYOMING + 495;ddulq-name;crh-firstname; 11540;yrm-city;1974/07/31;opds-street; 95;OH;OHIO + 496;fyrha-name;wea-firstname; 16620;vfe-city;1990/12/07;ukki-street; 48;GU;GUAM + 497;vuypf-name;ugz-firstname; 13320;ixw-city;1970/07/09;aptu-street; 60;HI;HAWAII + 498;wezwd-name;oae-firstname; 11180;egb-city;1982/02/27;ldea-street; 2;IL;ILLINOIS + 499;wrokv-name;zaa-firstname; 13980;hac-city;1974/01/20;zwst-street; 21;MO;MISSOURI + 500;wtapc-name;ciu-firstname; 15360;uvh-city;1962/08/03;mddz-street; 196;IA;IOWA + 501;hdtsu-name;two-firstname; 19500;ick-city;1958/11/07;kipe-street; 42;DE;DELAWARE + 502;lwprl-name;jaw-firstname; 14140;acy-city;1970/12/06;jhae-street; 129;MT;MONTANA + 503;iwftp-name;jnp-firstname; 18800;axg-city;1978/10/13;rejy-street; 174;AZ;ARIZONA + 504;qypws-name;fox-firstname; 19820;bzu-city;1966/01/02;owsq-street; 70;AR;ARKANSAS + 505;pzjcf-name;ese-firstname; 12180;kjq-city;1958/11/21;xbyg-street; 12;AS;AMERICAN SAMOA + 506;itzxd-name;erv-firstname; 10460;dsk-city;1978/06/20;baci-street; 151;OH;OHIO + 507;jnpdw-name;zna-firstname; 11000;aqt-city;1966/05/27;pukm-street; 80;WY;WYOMING + 508;dchwr-name;rxe-firstname; 19220;plm-city;1958/05/18;gkgx-street; 100;NH;NEW HAMPSHIRE + 509;pcszz-name;rym-firstname; 15860;tml-city;1983/01/25;qrdz-street; 7;RI;RHODE ISLAND + 510;fatdr-name;rcs-firstname; 17480;ajx-city;1958/09/18;nlal-street; 26;MD;MARYLAND + 511;oblzo-name;wwl-firstname; 17280;hxs-city;1958/06/15;rnpa-street; 20;AR;ARKANSAS + 512;nmflo-name;ljc-firstname; 19720;biq-city;1962/03/23;ypux-street; 197;OR;OREGON + 513;ajhdh-name;iba-firstname; 16920;yru-city;1982/09/08;zedq-street; 148;MN;MINNESOTA + 514;aiewz-name;gla-firstname; 19340;zvj-city;1986/01/12;blie-street; 116;MA;MASSACHUSETTS + 515;tglnr-name;fob-firstname; 14300;ejm-city;1986/09/22;zazt-street; 152;WV;WEST VIRGINIA + 516;tswnt-name;aal-firstname; 19940;jsw-city;1978/07/02;xnjc-street; 125;OK;OKLAHOMA + 517;smukz-name;zim-firstname; 15260;pul-city;1974/09/26;furv-street; 45;IA;IOWA + 518;aygln-name;qfk-firstname; 16700;bmu-city;1958/03/18;esys-street; 148;NE;NEBRASKA + 519;kcuub-name;ffc-firstname; 10720;xnk-city;1958/02/23;cefn-street; 135;AL;ALABAMA + 520;vsujm-name;yne-firstname; 11280;gdr-city;1966/02/12;hdah-street; 70;MS;MISSISSIPPI + 521;tirxh-name;gpy-firstname; 19360;bai-city;1970/04/19;gznh-street; 33;KY;KENTUCKY + 522;wlqnf-name;nnd-firstname; 16120;kij-city;1954/10/07;gorj-street; 85;ID;IDAHO + 523;rynel-name;iaq-firstname; 13640;chy-city;1954/02/01;wbiu-street; 62;WV;WEST VIRGINIA + 524;ohcvr-name;eod-firstname; 18240;lcc-city;1978/12/24;guca-street; 84;LA;LOUISIANA + 525;gabiw-name;gtj-firstname; 17860;ezv-city;1978/01/18;jsyv-street; 143;ND;NORTH DAKOTA + 526;ddajc-name;cab-firstname; 12920;fgz-city;1958/10/06;lkvp-street; 80;NV;NEVADA + 527;kivtx-name;pbs-firstname; 17980;mso-city;1982/10/22;qden-street; 69;IN;INDIANA + 528;tyial-name;mwb-firstname; 17080;pdf-city;1954/02/25;qyym-street; 48;MN;MINNESOTA + 529;ipbyo-name;lcr-firstname; 12040;ygz-city;1978/03/01;qbqk-street; 117;ND;NORTH DAKOTA + 530;tmkxn-name;jhl-firstname; 13460;xkh-city;1966/06/07;wkzu-street; 26;SD;SOUTH DAKOTA + 531;cnkac-name;jxe-firstname; 13140;vgf-city;1974/08/24;sifo-street; 84;ID;IDAHO + 532;zqnwe-name;ujv-firstname; 19020;vjy-city;1966/02/03;sfzz-street; 171;MN;MINNESOTA + 533;aphod-name;vsz-firstname; 11440;spc-city;1962/07/28;alnl-street; 152;FM;FEDERATED STATES OF MICRONESIA + 534;tjber-name;uhp-firstname; 19320;quo-city;1958/12/19;zgus-street; 173;FM;FEDERATED STATES OF MICRONESIA + 535;itblo-name;lbp-firstname; 14840;rgn-city;1962/11/29;qmtb-street; 1;IN;INDIANA + 536;navpk-name;wcs-firstname; 14400;nvr-city;1982/11/01;qlar-street; 126;AK;ALASKA + 537;nkyot-name;zep-firstname; 17720;yim-city;1954/09/30;wwdl-street; 198;ND;NORTH DAKOTA + 538;qebao-name;edw-firstname; 19220;tlh-city;1982/10/16;celb-street; 180;NJ;NEW JERSEY + 539;bzmvi-name;roq-firstname; 10240;beg-city;1974/07/10;nnom-street; 71;KY;KENTUCKY + 540;qtzxt-name;nau-firstname; 17240;fis-city;1990/05/27;fbex-street; 22;AR;ARKANSAS + 541;jarpd-name;mce-firstname; 17060;rgw-city;1982/10/20;eqbd-street; 180;WI;WISCONSIN + 542;ppaal-name;afq-firstname; 17300;fno-city;1982/10/01;zhby-street; 108;NY;NEW YORK + 543;sejhl-name;qwc-firstname; 18000;sfv-city;1986/04/13;xhlx-street; 52;CO;COLORADO + 544;qfvqk-name;mqn-firstname; 10540;rwu-city;1958/07/30;xwsy-street; 169;AL;ALABAMA + 545;ndowg-name;lnm-firstname; 13820;llz-city;1954/08/11;niif-street; 55;KS;KANSAS + 546;hutga-name;ztk-firstname; 19800;uci-city;1950/10/04;ywtn-street; 84;KS;KANSAS + 547;mudul-name;qcs-firstname; 13520;agk-city;1962/04/20;scto-street; 200;CO;COLORADO + 548;mnket-name;edm-firstname; 10040;jqb-city;1954/11/09;avkk-street; 23;VA;VIRGINIA + 549;mbxbn-name;yuu-firstname; 18880;oho-city;1970/11/24;heug-street; 116;PA;PENNSYLVANIA + 550;letcc-name;pyw-firstname; 10160;lzb-city;1966/06/11;wtul-street; 80;RI;RHODE ISLAND + 551;jqovo-name;eir-firstname; 14120;bvr-city;1974/07/01;wdxz-street; 156;SD;SOUTH DAKOTA + 552;jdkan-name;vxc-firstname; 15600;rqa-city;1962/02/07;vroo-street; 182;CO;COLORADO + 553;crzen-name;vwe-firstname; 18800;msz-city;1962/10/17;xxbp-street; 165;IA;IOWA + 554;nogoy-name;swt-firstname; 11580;npe-city;1978/04/15;uath-street; 55;NV;NEVADA + 555;qtkar-name;cdv-firstname; 17020;opk-city;1950/07/26;bsnt-street; 17;AK;ALASKA + 556;oigrk-name;zmz-firstname; 14200;zhd-city;1966/11/30;yeqk-street; 12;DC;DISTRICT OF COLUMBIA + 557;qspmb-name;htl-firstname; 10940;oxl-city;1986/11/17;bqdp-street; 71;NH;NEW HAMPSHIRE + 558;qktka-name;ces-firstname; 12840;xcq-city;1986/02/13;ddqq-street; 62;NE;NEBRASKA + 559;bokzl-name;vfw-firstname; 14160;eyd-city;1970/02/15;ffrm-street; 26;IL;ILLINOIS + 560;eksje-name;xxw-firstname; 18600;vtn-city;1962/12/09;nskq-street; 42;VA;VIRGINIA + 561;avxuw-name;niq-firstname; 13520;cyx-city;1958/05/29;dxit-street; 167;CA;CALIFORNIA + 562;xwskh-name;mud-firstname; 10900;bcd-city;1954/02/15;lqcd-street; 5;VA;VIRGINIA + 563;jrlgi-name;qbl-firstname; 11780;xkb-city;1970/01/27;wfti-street; 101;CO;COLORADO + 564;vvgef-name;qbi-firstname; 17580;dgu-city;1974/10/21;pkhw-street; 140;DE;DELAWARE + 565;vbdsa-name;mqy-firstname; 19520;iqf-city;1958/06/24;gnhg-street; 23;AL;ALABAMA + 566;mimxv-name;idp-firstname; 19280;acp-city;1986/02/10;wziw-street; 1;LA;LOUISIANA + 567;ihvtb-name;jdf-firstname; 17540;xpg-city;1970/11/23;pjvm-street; 156;SC;SOUTH CAROLINA + 568;wztgo-name;njm-firstname; 15200;btu-city;1978/10/30;ihdb-street; 25;OH;OHIO + 569;unnnh-name;zry-firstname; 12300;upy-city;1982/09/15;nbjp-street; 91;GU;GUAM + 570;qtsep-name;spo-firstname; 15940;ryi-city;1983/01/26;kvcq-street; 94;FM;FEDERATED STATES OF MICRONESIA + 571;rznvs-name;cjh-firstname; 15180;duz-city;1974/05/12;igjl-street; 111;AL;ALABAMA + 572;yrdmb-name;lvl-firstname; 12240;pzi-city;1962/11/10;kegb-street; 54;NY;NEW YORK + 573;jdhso-name;hms-firstname; 12620;ova-city;1950/02/14;rxks-street; 115;MS;MISSISSIPPI + 574;seigu-name;rwp-firstname; 15620;yrt-city;1958/09/30;mloc-street; 9;NJ;NEW JERSEY + 575;wzura-name;fzo-firstname; 12500;vgt-city;1974/06/01;tevl-street; 50;NC;NORTH CAROLINA + 576;urqhu-name;hiq-firstname; 15020;ejs-city;1966/04/08;khev-street; 38;AK;ALASKA + 577;yazce-name;zhy-firstname; 12920;ytz-city;1982/05/07;rfsh-street; 125;NH;NEW HAMPSHIRE + 578;okxei-name;qbi-firstname; 17400;plc-city;1966/04/05;mvzs-street; 73;KS;KANSAS + 579;eemhh-name;nsj-firstname; 11660;buq-city;1986/06/23;mftt-street; 191;NE;NEBRASKA + 580;uyoei-name;wiz-firstname; 19000;lqo-city;1974/06/18;rvat-street; 197;NC;NORTH CAROLINA + 581;ahbez-name;gwg-firstname; 10940;gno-city;1978/03/23;rfwr-street; 105;KS;KANSAS + 582;qnopb-name;xgc-firstname; 18800;ayx-city;1962/05/30;lrnc-street; 193;TN;TENNESSEE + 583;beqpz-name;psc-firstname; 10880;sfe-city;1986/07/04;tobm-street; 121;AK;ALASKA + 584;nnvir-name;tbu-firstname; 12860;cuj-city;1962/04/25;wuxg-street; 11;WA;WASHINGTON + 585;cmqrt-name;huk-firstname; 19580;kae-city;1958/09/29;yukq-street; 146;IA;IOWA + 586;ifivh-name;bub-firstname; 15640;pti-city;1950/04/21;alaw-street; 104;VT;VERMONT + 587;nqbqs-name;ndv-firstname; 17500;sxr-city;1963/01/05;zclw-street; 77;MP;NORTHERN MARIANA ISLANDS + 588;kgrlf-name;suz-firstname; 16080;ayf-city;1990/03/07;vmto-street; 120;CO;COLORADO + 589;nhjsx-name;xuh-firstname; 12360;tqo-city;1970/08/04;vfzt-street; 181;undefined;undefined + 590;vdint-name;zal-firstname; 14600;fjh-city;1982/04/22;uopg-street; 117;WV;WEST VIRGINIA + 591;nchqr-name;ldr-firstname; 14700;pyy-city;1966/03/02;nlzm-street; 137;VT;VERMONT + 592;sjmbq-name;tsc-firstname; 19960;dlp-city;1974/05/26;qphd-street; 155;IN;INDIANA + 593;njbsk-name;oft-firstname; 17280;xtc-city;1958/02/13;sebl-street; 85;HI;HAWAII + 594;vtijb-name;obv-firstname; 14000;bpn-city;1962/08/08;mxrl-street; 199;IL;ILLINOIS + 595;ihqjn-name;hnu-firstname; 18960;ztv-city;1982/02/20;mhrl-street; 45;MA;MASSACHUSETTS + 596;kpzko-name;zfz-firstname; 18720;rjc-city;1971/01/01;ycrf-street; 173;CT;CONNECTICUT + 597;qlfgm-name;fnv-firstname; 11360;avw-city;1970/12/01;yvgg-street; 49;LA;LOUISIANA + 598;oeepm-name;asq-firstname; 18660;mvq-city;1990/07/29;ssey-street; 57;OH;OHIO + 599;waklo-name;ksz-firstname; 10660;ddf-city;1958/07/10;dilr-street; 145;NY;NEW YORK + 600;mdftt-name;ldq-firstname; 11460;mkm-city;1966/09/03;rjkt-street; 9;CA;CALIFORNIA + 601;iowsr-name;vmg-firstname; 18420;kie-city;1954/12/30;qfom-street; 117;NE;NEBRASKA + 602;sdtxj-name;zbp-firstname; 16160;gic-city;1962/09/09;eavm-street; 108;IA;IOWA + 603;ocebd-name;img-firstname; 12180;rxp-city;1966/08/18;ygmy-street; 52;PA;PENNSYLVANIA + 604;uvovz-name;nsd-firstname; 13600;tlc-city;1950/08/13;xbsm-street; 110;MP;NORTHERN MARIANA ISLANDS + 605;fzuuf-name;pub-firstname; 10500;ikg-city;1974/04/26;pmnl-street; 189;MA;MASSACHUSETTS + 606;xzvbs-name;qpb-firstname; 17060;xed-city;1950/04/18;vvdx-street; 118;OR;OREGON + 607;qvkir-name;hns-firstname; 12840;kxi-city;1978/03/02;kzbt-street; 65;RI;RHODE ISLAND + 608;llarb-name;psp-firstname; 11240;wlq-city;1990/10/10;qshw-street; 106;AL;ALABAMA + 609;wrzts-name;ygm-firstname; 15520;ybe-city;1974/12/03;uxct-street; 200;ND;NORTH DAKOTA + 610;qfpdp-name;luy-firstname; 16260;qma-city;1978/07/12;opsm-street; 194;RI;RHODE ISLAND + 611;xaosj-name;kfd-firstname; 18100;xmf-city;1982/12/22;hceh-street; 75;LA;LOUISIANA + 612;iuocr-name;ztn-firstname; 19000;sby-city;1970/06/07;qres-street; 86;TX;TEXAS + 613;ocvrm-name;ylm-firstname; 16360;vif-city;1986/11/17;zgov-street; 17;CT;CONNECTICUT + 614;tealr-name;lbu-firstname; 10020;blg-city;1958/03/23;qjvy-street; 182;MS;MISSISSIPPI + 615;ksjja-name;dky-firstname; 15360;qim-city;1986/08/30;rvtm-street; 182;KY;KENTUCKY + 616;jbhpa-name;shc-firstname; 18020;gmc-city;1974/05/29;cvpl-street; 154;AS;AMERICAN SAMOA + 617;vraon-name;nam-firstname; 16320;joa-city;1986/11/07;pnik-street; 194;NM;NEW MEXICO + 618;gtymv-name;lgo-firstname; 16740;ynr-city;1970/05/14;xxzj-street; 74;NJ;NEW JERSEY + 619;sxbpo-name;hyx-firstname; 12900;lvu-city;1958/07/14;tysz-street; 68;HI;HAWAII + 620;yaogj-name;fjg-firstname; 11040;slr-city;1974/06/22;kjoi-street; 134;OK;OKLAHOMA + 621;rprrj-name;vca-firstname; 19160;txr-city;1959/01/18;stjh-street; 137;MA;MASSACHUSETTS + 622;dbaaq-name;jdi-firstname; 13800;uge-city;1954/11/08;ftck-street; 175;OK;OKLAHOMA + 623;eapsq-name;zeu-firstname; 14720;mmh-city;1954/11/01;tjlj-street; 84;AS;AMERICAN SAMOA + 624;wlrqb-name;srf-firstname; 15280;bbq-city;1970/04/06;thvb-street; 17;CO;COLORADO + 625;dzzia-name;ukn-firstname; 14680;zug-city;1954/04/29;nthu-street; 109;LA;LOUISIANA + 626;nfzxa-name;ikc-firstname; 17080;sqt-city;1982/01/21;dteu-street; 113;CT;CONNECTICUT + 627;ovxiz-name;lho-firstname; 10620;gox-city;1954/07/05;cofd-street; 155;NY;NEW YORK + 628;msubu-name;ccj-firstname; 17780;jkv-city;1982/02/25;gfve-street; 43;NV;NEVADA + 629;xxbdr-name;kas-firstname; 17320;ick-city;1970/02/25;gvoi-street; 24;ID;IDAHO + 630;rgryb-name;guv-firstname; 11160;xus-city;1966/05/06;plka-street; 164;FL;FLORIDA + 631;dxprz-name;byy-firstname; 16520;lpb-city;1974/03/01;auoq-street; 103;MH;MARSHALL ISLANDS + 632;nerdb-name;eqa-firstname; 12860;arg-city;1958/03/17;emcz-street; 104;DC;DISTRICT OF COLUMBIA + 633;nszhh-name;nrn-firstname; 16860;hap-city;1974/04/30;zjuq-street; 78;IA;IOWA + 634;jtcxq-name;hxi-firstname; 12160;oqq-city;1982/06/03;xpsp-street; 30;VI;VIRGIN ISLANDS + 635;prglc-name;kqd-firstname; 16080;gqr-city;1978/10/14;pjin-street; 145;NH;NEW HAMPSHIRE + 636;yofsk-name;qch-firstname; 14240;wbz-city;1990/05/28;edsh-street; 5;MI;MICHIGAN + 637;cupcm-name;jvy-firstname; 13720;lkw-city;1966/10/14;frzx-street; 83;NY;NEW YORK + 638;crjjf-name;yfe-firstname; 19880;oof-city;1970/02/07;akkj-street; 98;NC;NORTH CAROLINA + 639;rqyyg-name;hbx-firstname; 12080;sxh-city;1958/04/24;wzjb-street; 159;TX;TEXAS + 640;xxugu-name;yqf-firstname; 10380;ocx-city;1954/11/18;mbps-street; 20;ND;NORTH DAKOTA + 641;ayvhk-name;znx-firstname; 16960;qbw-city;1954/07/23;wgsh-street; 148;OK;OKLAHOMA + 642;xwdxq-name;aik-firstname; 14280;djc-city;1958/06/18;mjpa-street; 130;OH;OHIO + 643;qsvmf-name;lgn-firstname; 17140;mpl-city;1982/06/15;ngot-street; 193;NJ;NEW JERSEY + 644;tlued-name;mcz-firstname; 16780;oab-city;1978/09/22;ppzd-street; 111;AR;ARKANSAS + 645;ibhei-name;djd-firstname; 11340;eep-city;1975/01/24;rzdt-street; 94;MH;MARSHALL ISLANDS + 646;snfrd-name;jof-firstname; 12700;lck-city;1978/12/16;jpcu-street; 184;IN;INDIANA + 647;vkwed-name;elj-firstname; 18940;tdi-city;1990/08/05;ekyx-street; 137;KY;KENTUCKY + 648;acing-name;rxv-firstname; 10660;gtt-city;1970/11/17;ltxb-street; 117;VA;VIRGINIA + 649;fzmkk-name;ilb-firstname; 18540;rwj-city;1986/02/08;xsnm-street; 113;MA;MASSACHUSETTS + 650;knzxd-name;seg-firstname; 17180;oqu-city;1982/11/02;ukda-street; 80;MT;MONTANA + 651;zzyix-name;dlk-firstname; 14180;yoc-city;1986/10/06;jnpv-street; 71;FM;FEDERATED STATES OF MICRONESIA + 652;nsote-name;ivj-firstname; 19740;aqh-city;1990/11/05;faox-street; 77;WI;WISCONSIN + 653;znees-name;edg-firstname; 18120;qkr-city;1986/12/08;prpo-street; 42;DE;DELAWARE + 654;svzbm-name;hsg-firstname; 19180;shh-city;1954/02/05;uglh-street; 150;IA;IOWA + 655;llfur-name;geu-firstname; 12340;qdf-city;1970/12/01;nntt-street; 92;AZ;ARIZONA + 656;pusrl-name;ovn-firstname; 17020;oaa-city;1954/07/30;oxfp-street; 104;AL;ALABAMA + 657;wihqb-name;bkq-firstname; 13540;jau-city;1974/09/25;vuga-street; 83;NY;NEW YORK + 658;ezgdt-name;yaa-firstname; 13380;fdc-city;1986/11/04;yxut-street; 75;VI;VIRGIN ISLANDS + 659;kvtlv-name;zdr-firstname; 15700;bqd-city;1990/06/04;kajd-street; 92;DE;DELAWARE + 660;bqkzl-name;tym-firstname; 14900;rhk-city;1962/02/13;yyzk-street; 165;MH;MARSHALL ISLANDS + 661;ndfve-name;ajt-firstname; 19460;tdr-city;1970/02/04;vpub-street; 143;CT;CONNECTICUT + 662;temdh-name;trc-firstname; 11720;ald-city;1950/05/06;tapb-street; 1;FL;FLORIDA + 663;bzagi-name;dwa-firstname; 13580;zvq-city;1970/05/05;khyi-street; 105;CT;CONNECTICUT + 664;frtvn-name;ukn-firstname; 19400;eto-city;1986/06/29;dcnz-street; 114;MH;MARSHALL ISLANDS + 665;mjdqu-name;xlw-firstname; 14420;lpe-city;1962/08/02;oecb-street; 189;IN;INDIANA + 666;fnopj-name;sdh-firstname; 19080;hxm-city;1974/11/24;pfxz-street; 66;ME;MAINE + 667;kuxaz-name;hye-firstname; 14860;eia-city;1962/09/16;nhdg-street; 198;CO;COLORADO + 668;tfrpe-name;mme-firstname; 12860;mox-city;1978/03/24;uhqo-street; 10;NE;NEBRASKA + 669;eodve-name;tzj-firstname; 11440;syb-city;1986/07/29;jlnw-street; 74;WI;WISCONSIN + 670;slzsi-name;nvw-firstname; 13680;kgz-city;1958/07/13;pptx-street; 88;NY;NEW YORK + 671;wrttz-name;wwx-firstname; 13440;lig-city;1986/11/05;vdph-street; 66;MI;MICHIGAN + 672;wfxhy-name;wtc-firstname; 12760;gnf-city;1970/08/22;itut-street; 3;HI;HAWAII + 673;toeja-name;vrw-firstname; 18900;xes-city;1982/09/01;wpqu-street; 184;undefined;undefined + 674;glmnt-name;bck-firstname; 11680;cuu-city;1978/08/02;wtxv-street; 16;RI;RHODE ISLAND + 675;vyvnk-name;shl-firstname; 10260;xhq-city;1970/06/06;ddbz-street; 28;MA;MASSACHUSETTS + 676;pjdvs-name;adv-firstname; 15600;snf-city;1986/12/10;tdft-street; 133;DC;DISTRICT OF COLUMBIA + 677;rulrt-name;ytn-firstname; 19340;spb-city;1974/11/11;bpwx-street; 24;VA;VIRGINIA + 678;gvttx-name;moj-firstname; 17000;gri-city;1978/11/24;kqlt-street; 27;ID;IDAHO + 679;jtlah-name;gxq-firstname; 15440;jte-city;1978/02/01;mbvv-street; 172;OH;OHIO + 680;cpvhs-name;ruo-firstname; 16080;vhq-city;1970/10/18;ditw-street; 164;AR;ARKANSAS + 681;jikyx-name;hzl-firstname; 13320;hgh-city;1958/08/07;gmqp-street; 85;FL;FLORIDA + 682;bsvsn-name;idb-firstname; 15780;ooc-city;1958/04/28;apff-street; 78;MO;MISSOURI + 683;qgivn-name;bau-firstname; 11580;uez-city;1987/01/29;vpsa-street; 48;RI;RHODE ISLAND + 684;hqbfl-name;fgr-firstname; 11620;ejc-city;1982/02/07;fwsq-street; 107;WA;WASHINGTON + 685;crmtk-name;fmd-firstname; 14480;mdp-city;1962/05/23;knxw-street; 18;IL;ILLINOIS + 686;kccgj-name;pzd-firstname; 10760;ifv-city;1990/05/14;dgfl-street; 172;PR;PUERTO RICO + 687;mddbw-name;lcu-firstname; 11360;crz-city;1970/08/24;eiwf-street; 32;PR;PUERTO RICO + 688;dlnxj-name;tzx-firstname; 11380;nza-city;1966/09/25;jlna-street; 186;MO;MISSOURI + 689;wpjcx-name;hwx-firstname; 13100;slr-city;1987/01/16;wxcr-street; 42;AK;ALASKA + 690;yefnh-name;jzp-firstname; 13080;ozf-city;1986/08/09;zzos-street; 120;OR;OREGON + 691;qsoei-name;kio-firstname; 11860;esa-city;1974/08/04;bhbl-street; 86;HI;HAWAII + 692;xwodh-name;aid-firstname; 17940;wgd-city;1954/07/21;rcrf-street; 90;IL;ILLINOIS + 693;oncpb-name;elk-firstname; 12020;rbp-city;1974/10/28;iblz-street; 114;DE;DELAWARE + 694;qisar-name;kfc-firstname; 19800;alw-city;1958/08/07;ukbj-street; 53;NH;NEW HAMPSHIRE + 695;bvsga-name;qln-firstname; 18060;zlt-city;1966/01/29;rngf-street; 46;WA;WASHINGTON + 696;xguku-name;uxf-firstname; 10080;ezg-city;1974/12/30;icvq-street; 48;UT;UTAH + 697;hxylt-name;hng-firstname; 15360;jgc-city;1966/03/11;lhxd-street; 87;DE;DELAWARE + 698;dmlyv-name;qai-firstname; 20000;hoe-city;1966/08/20;uboj-street; 72;KY;KENTUCKY + 699;kkbjx-name;cnx-firstname; 16780;ndu-city;1970/08/16;afzw-street; 55;NJ;NEW JERSEY + 700;emoio-name;awr-firstname; 18560;qpj-city;1974/11/17;hasx-street; 92;VT;VERMONT + 701;lxpni-name;oox-firstname; 14100;xrj-city;1986/06/09;wvvi-street; 106;MO;MISSOURI + 702;oshuu-name;mim-firstname; 14280;mns-city;1982/09/12;ashj-street; 95;FM;FEDERATED STATES OF MICRONESIA + 703;uhpxg-name;gnt-firstname; 19180;zsi-city;1978/05/22;namc-street; 126;AL;ALABAMA + 704;lvoek-name;reg-firstname; 18920;ubj-city;1954/12/24;nrjw-street; 38;PA;PENNSYLVANIA + 705;lpnhx-name;szp-firstname; 10720;ybd-city;1970/11/26;ntyc-street; 109;GA;GEORGIA + 706;kppvg-name;ztz-firstname; 14660;ocp-city;1990/08/19;ekcr-street; 56;SD;SOUTH DAKOTA + 707;vxwxv-name;qzo-firstname; 15540;rfl-city;1962/03/29;hcwy-street; 116;KY;KENTUCKY + 708;nbgan-name;ocf-firstname; 15240;aas-city;1954/09/14;zkkh-street; 117;IN;INDIANA + 709;faiaw-name;lup-firstname; 16980;rsz-city;1986/09/17;yddi-street; 98;IN;INDIANA + 710;mdmax-name;ggz-firstname; 17360;stn-city;1966/11/25;lwhf-street; 198;IN;INDIANA + 711;etauo-name;pta-firstname; 13360;fnh-city;1978/02/03;gsrn-street; 113;IA;IOWA + 712;gckpz-name;rou-firstname; 17920;liu-city;1974/10/10;apjm-street; 40;AZ;ARIZONA + 713;szvhz-name;ani-firstname; 17240;nab-city;1958/12/10;cxho-street; 87;KY;KENTUCKY + 714;dawkl-name;epe-firstname; 11780;pjg-city;1990/04/24;hdbx-street; 104;WI;WISCONSIN + 715;eifes-name;zka-firstname; 11500;uux-city;1986/10/20;pazo-street; 125;TN;TENNESSEE + 716;fphzw-name;gof-firstname; 14680;dkz-city;1986/06/23;aejx-street; 88;VA;VIRGINIA + 717;bgicm-name;one-firstname; 14820;lwh-city;1966/01/08;dizm-street; 53;AR;ARKANSAS + 718;qnozr-name;fwa-firstname; 17800;rsm-city;1954/04/11;tuun-street; 63;NE;NEBRASKA + 719;zxcjb-name;mhs-firstname; 18620;czu-city;1982/11/15;ucfg-street; 103;PW;PALAU + 720;qqftw-name;dox-firstname; 11280;eii-city;1982/08/03;akjj-street; 174;VT;VERMONT + 721;byevj-name;bah-firstname; 14240;rge-city;1958/01/21;rkkm-street; 142;MD;MARYLAND + 722;ihvvb-name;gaq-firstname; 19140;gua-city;1958/09/30;hxim-street; 88;RI;RHODE ISLAND + 723;suiee-name;vji-firstname; 12120;zuz-city;1970/02/22;ijcn-street; 84;PW;PALAU + 724;fgkoi-name;xkx-firstname; 15880;zuk-city;1970/12/27;frfg-street; 74;IL;ILLINOIS + 725;aowhf-name;hvv-firstname; 12740;mpj-city;1987/01/06;mrmx-street; 95;KY;KENTUCKY + 726;vxkbl-name;xfk-firstname; 18600;odo-city;1970/02/28;jrzc-street; 128;AZ;ARIZONA + 727;zpedp-name;oxh-firstname; 16380;cjz-city;1958/05/28;bnxj-street; 18;GU;GUAM + 728;uqfju-name;dmq-firstname; 15560;ehf-city;1954/03/02;ptqn-street; 68;VA;VIRGINIA + 729;wsoxm-name;xtd-firstname; 15660;ogk-city;1951/01/29;inkh-street; 150;WV;WEST VIRGINIA + 730;cvypk-name;xoy-firstname; 15080;cwr-city;1970/02/28;kyio-street; 117;WV;WEST VIRGINIA + 731;yprgp-name;vgc-firstname; 14660;alm-city;1971/01/05;uoxp-street; 110;CA;CALIFORNIA + 732;udhil-name;dsa-firstname; 12060;lzv-city;1974/02/18;gvvc-street; 149;AK;ALASKA + 733;fgtjr-name;bnt-firstname; 18240;pgf-city;1986/10/15;synp-street; 159;CT;CONNECTICUT + 734;hypgs-name;ycz-firstname; 10840;ufx-city;1966/06/26;frur-street; 81;HI;HAWAII + 735;ijqas-name;bgl-firstname; 18680;dss-city;1950/02/20;rafi-street; 134;ID;IDAHO + 736;seeae-name;pvg-firstname; 10800;txd-city;1958/10/18;uibx-street; 102;GU;GUAM + 737;punzk-name;ubx-firstname; 15020;qzq-city;1978/07/13;cqww-street; 52;PA;PENNSYLVANIA + 738;xyvjy-name;jta-firstname; 19700;koe-city;1986/06/22;lrjw-street; 17;undefined;undefined + 739;ysnek-name;kop-firstname; 13120;bel-city;1950/08/01;xcbk-street; 94;MA;MASSACHUSETTS + 740;ozxng-name;sje-firstname; 15180;nqf-city;1986/11/19;uivr-street; 117;MP;NORTHERN MARIANA ISLANDS + 741;ekqcg-name;hxq-firstname; 16880;jic-city;1966/01/11;grbw-street; 86;UT;UTAH + 742;xxhts-name;krw-firstname; 14140;lwz-city;1978/04/06;fzbe-street; 40;WI;WISCONSIN + 743;uffha-name;jdu-firstname; 13080;jmd-city;1986/06/21;sqbk-street; 66;KS;KANSAS + 744;aevbo-name;bba-firstname; 16880;hgh-city;1958/03/08;urgb-street; 74;NE;NEBRASKA + 745;rqwnb-name;zpb-firstname; 19300;cbx-city;1978/08/12;qavq-street; 5;IN;INDIANA + 746;txozw-name;vet-firstname; 15540;ajt-city;1954/07/05;jayh-street; 105;RI;RHODE ISLAND + 747;xmyzv-name;phl-firstname; 15220;jnk-city;1974/04/04;ckwm-street; 174;KY;KENTUCKY + 748;fusnk-name;iva-firstname; 14660;kqd-city;1982/07/03;zmbq-street; 10;FL;FLORIDA + 749;ksnje-name;eaq-firstname; 12500;gej-city;1982/10/03;vbwg-street; 83;HI;HAWAII + 750;kcahb-name;srs-firstname; 11300;bvz-city;1978/05/16;gyfr-street; 164;LA;LOUISIANA + 751;bzjht-name;vfn-firstname; 17500;mgi-city;1958/06/15;mmko-street; 172;DE;DELAWARE + 752;loydq-name;gft-firstname; 11900;fsr-city;1990/01/20;sjds-street; 62;TN;TENNESSEE + 753;kglxv-name;zlt-firstname; 14960;qgb-city;1982/02/07;gniz-street; 178;AS;AMERICAN SAMOA + 754;loimm-name;ipg-firstname; 15980;sxt-city;1978/10/31;kdlk-street; 22;NY;NEW YORK + 755;demtf-name;yhq-firstname; 12300;pdo-city;1982/09/18;mfye-street; 99;TN;TENNESSEE + 756;jdmdi-name;llu-firstname; 18060;pss-city;1974/07/01;utrs-street; 198;CA;CALIFORNIA + 757;crjfv-name;ccn-firstname; 19520;uub-city;1978/07/12;xtvk-street; 138;OK;OKLAHOMA + 758;irghm-name;dbi-firstname; 15200;otd-city;1982/12/14;iple-street; 144;WY;WYOMING + 759;mcwlu-name;kxy-firstname; 17640;edu-city;1958/07/05;ibtg-street; 173;IN;INDIANA + 760;plfvg-name;cxa-firstname; 19360;rnc-city;1958/04/06;zemc-street; 64;DE;DELAWARE + 761;dnxai-name;gnd-firstname; 11580;ers-city;1966/07/14;ytjt-street; 163;OH;OHIO + 762;ksbwq-name;tku-firstname; 10040;axu-city;1974/08/10;kvel-street; 95;RI;RHODE ISLAND + 763;durgp-name;unr-firstname; 12820;juk-city;1959/01/05;ucky-street; 132;HI;HAWAII + 764;eocqj-name;qcg-firstname; 18940;faf-city;1986/10/14;gjjy-street; 126;OK;OKLAHOMA + 765;dhcim-name;eti-firstname; 17140;aed-city;1982/07/19;syxk-street; 156;FL;FLORIDA + 766;nerhu-name;jfx-firstname; 10140;huk-city;1978/02/07;cyfs-street; 47;ME;MAINE + 767;sxukr-name;hag-firstname; 14400;lfc-city;1963/01/05;wylb-street; 38;WV;WEST VIRGINIA + 768;xznte-name;snc-firstname; 17840;mhl-city;1962/03/23;rfig-street; 151;DE;DELAWARE + 769;tgwig-name;rfn-firstname; 14020;fcs-city;1986/11/12;agpo-street; 79;undefined;undefined + 770;kmkbd-name;fzv-firstname; 12920;kjt-city;1974/03/03;sgjo-street; 63;MN;MINNESOTA + 771;kqzne-name;gvz-firstname; 15500;zar-city;1966/04/01;kvlk-street; 35;DC;DISTRICT OF COLUMBIA + 772;hatuj-name;fwt-firstname; 16040;gih-city;1978/07/10;wbjj-street; 70;MD;MARYLAND + 773;anvjr-name;qqu-firstname; 15280;php-city;1970/09/28;hhnz-street; 147;GU;GUAM + 774;jxiox-name;ein-firstname; 18560;vou-city;1974/04/29;dlcm-street; 52;VA;VIRGINIA + 775;ahdci-name;fhq-firstname; 10040;dis-city;1974/05/17;flyu-street; 84;MO;MISSOURI + 776;slvkr-name;qtq-firstname; 10800;seu-city;1978/08/24;hivm-street; 148;PR;PUERTO RICO + 777;xncyt-name;alj-firstname; 12500;qcq-city;1962/05/20;bqjo-street; 78;VI;VIRGIN ISLANDS + 778;oagkd-name;oud-firstname; 10720;nai-city;1974/09/12;jbps-street; 50;MT;MONTANA + 779;bhinx-name;sde-firstname; 18240;zrt-city;1962/07/08;dxuq-street; 97;PA;PENNSYLVANIA + 780;wpyvx-name;qwg-firstname; 18520;lku-city;1966/03/09;ymkt-street; 133;MD;MARYLAND + 781;wvkan-name;ndd-firstname; 10340;hwn-city;1978/11/18;pqqf-street; 114;NV;NEVADA + 782;biapu-name;kiq-firstname; 16360;lbi-city;1954/10/27;pawe-street; 198;OK;OKLAHOMA + 783;ohygc-name;vxw-firstname; 11100;flm-city;1966/11/20;homq-street; 170;HI;HAWAII + 784;ntbgw-name;msg-firstname; 13740;cth-city;1958/09/28;epdz-street; 80;AS;AMERICAN SAMOA + 785;vegli-name;avd-firstname; 18000;gob-city;1970/08/27;bvnt-street; 32;WY;WYOMING + 786;aadoy-name;gre-firstname; 16040;qln-city;1962/01/03;lqxs-street; 179;ME;MAINE + 787;rzwid-name;xjl-firstname; 16300;eds-city;1970/05/08;hutz-street; 27;TX;TEXAS + 788;qperl-name;nsk-firstname; 13920;hxo-city;1966/11/16;xvzr-street; 160;IA;IOWA + 789;hkhzc-name;hxd-firstname; 19980;gpm-city;1954/08/23;qmrm-street; 60;VA;VIRGINIA + 790;qlixr-name;tsd-firstname; 16780;dvv-city;1978/02/14;ibwa-street; 85;AZ;ARIZONA + 791;ieyhf-name;oox-firstname; 19820;mfp-city;1958/02/17;ntdb-street; 100;MD;MARYLAND + 792;xxtwm-name;zpp-firstname; 12980;udl-city;1970/03/01;dkzb-street; 157;DC;DISTRICT OF COLUMBIA + 793;hvtaa-name;wcr-firstname; 14460;zek-city;1970/01/18;bayt-street; 149;PR;PUERTO RICO + 794;xduae-name;iqk-firstname; 18380;lkm-city;1982/11/24;aujb-street; 62;GA;GEORGIA + 795;qkjcz-name;abw-firstname; 15080;fco-city;1986/11/01;tkuc-street; 69;FM;FEDERATED STATES OF MICRONESIA + 796;jplol-name;akr-firstname; 19760;rvj-city;1970/12/23;sqxe-street; 128;SD;SOUTH DAKOTA + 797;iegsb-name;ujo-firstname; 18480;abx-city;1974/02/24;esda-street; 16;RI;RHODE ISLAND + 798;ypqix-name;hbp-firstname; 13240;kdv-city;1986/05/20;iyaq-street; 10;OH;OHIO + 799;awluy-name;ysq-firstname; 13680;kcv-city;1986/12/16;rtbk-street; 144;ID;IDAHO + 800;vtyuq-name;cup-firstname; 13200;gcq-city;1970/11/05;omwn-street; 156;AR;ARKANSAS + 801;hmvzq-name;jgx-firstname; 18660;hbj-city;1982/03/30;hxik-street; 59;MO;MISSOURI + 802;vhatl-name;ywe-firstname; 10040;ytj-city;1982/02/10;lblz-street; 62;CT;CONNECTICUT + 803;pvozw-name;pln-firstname; 13980;tdu-city;1986/06/18;ulqg-street; 8;MT;MONTANA + 804;mwmvk-name;ttp-firstname; 14020;dcw-city;1982/08/26;lqrt-street; 104;CO;COLORADO + 805;pmlsn-name;uxe-firstname; 17680;udv-city;1962/12/27;usnv-street; 155;PR;PUERTO RICO + 806;uuvrf-name;jxu-firstname; 16360;vpo-city;1978/08/04;tpez-street; 25;MS;MISSISSIPPI + 807;pvcck-name;nnx-firstname; 19780;kfm-city;1962/07/05;aybu-street; 151;CA;CALIFORNIA + 808;kdybn-name;qcg-firstname; 11080;wno-city;1978/05/06;omzx-street; 56;GU;GUAM + 809;ylapy-name;mou-firstname; 11400;yky-city;1990/03/12;rcbx-street; 167;WI;WISCONSIN + 810;ygvfd-name;hqp-firstname; 13240;sfw-city;1958/05/16;svut-street; 16;RI;RHODE ISLAND + 811;wgbsf-name;wgj-firstname; 12000;itm-city;1966/07/31;xlwg-street; 122;NV;NEVADA + 812;guhzx-name;hde-firstname; 19260;kqz-city;1962/03/01;escf-street; 84;NY;NEW YORK + 813;quyjf-name;vta-firstname; 14440;yiw-city;1978/01/29;nsss-street; 83;ND;NORTH DAKOTA + 814;fsbhc-name;ink-firstname; 19220;qjo-city;1974/08/30;pwtn-street; 140;WA;WASHINGTON + 815;zhzwu-name;pkl-firstname; 17680;slh-city;1966/06/13;cspv-street; 106;NJ;NEW JERSEY + 816;sqegy-name;ufg-firstname; 19220;hry-city;1954/03/25;anrd-street; 173;PW;PALAU + 817;bkiln-name;sjs-firstname; 15100;fao-city;1958/09/04;inav-street; 170;AR;ARKANSAS + 818;zynej-name;zgh-firstname; 15100;spc-city;1962/01/25;zewf-street; 85;GU;GUAM + 819;xzasj-name;kib-firstname; 18080;rzn-city;1970/10/07;tlsi-street; 190;SD;SOUTH DAKOTA + 820;tmqlc-name;jes-firstname; 11560;vhc-city;1974/11/27;xxkh-street; 36;MO;MISSOURI + 821;gowim-name;rcr-firstname; 11200;kja-city;1974/06/06;zhkg-street; 41;MO;MISSOURI + 822;bulti-name;cse-firstname; 13660;ukw-city;1970/12/14;glrc-street; 59;NV;NEVADA + 823;evxmh-name;gfc-firstname; 13560;cbo-city;1986/03/05;sfkz-street; 72;ID;IDAHO + 824;dbdop-name;swu-firstname; 14760;tlv-city;1982/10/06;vprj-street; 87;AR;ARKANSAS + 825;mgyeq-name;wko-firstname; 15320;lxz-city;1990/03/22;mfwa-street; 11;DE;DELAWARE + 826;bfplt-name;ard-firstname; 10360;sou-city;1990/02/13;jniu-street; 124;NC;NORTH CAROLINA + 827;dvbot-name;auq-firstname; 16200;qkn-city;1978/03/20;qfzt-street; 9;IL;ILLINOIS + 828;tncjm-name;kvn-firstname; 11640;mlf-city;1954/02/08;igos-street; 166;ID;IDAHO + 829;svoxr-name;rbc-firstname; 10400;bny-city;1958/07/13;socq-street; 198;MH;MARSHALL ISLANDS + 830;jxzxn-name;wau-firstname; 14360;orx-city;1958/07/08;fssh-street; 77;NM;NEW MEXICO + 831;dglke-name;dyc-firstname; 13100;lgz-city;1982/12/18;qixe-street; 158;MH;MARSHALL ISLANDS + 832;pewvm-name;asq-firstname; 10840;czn-city;1966/04/02;wnac-street; 84;MD;MARYLAND + 833;tfgsc-name;vxn-firstname; 10100;wup-city;1982/02/05;qlsb-street; 193;AK;ALASKA + 834;hmoxn-name;vti-firstname; 13660;gvq-city;1974/11/25;dahy-street; 123;HI;HAWAII + 835;bexku-name;mvy-firstname; 13080;wrx-city;1966/09/25;ovag-street; 86;GA;GEORGIA + 836;ksffn-name;ubg-firstname; 18600;gsu-city;1958/02/03;vepx-street; 58;NE;NEBRASKA + 837;ngfsu-name;pgq-firstname; 10400;pag-city;1986/06/19;rwqz-street; 123;DE;DELAWARE + 838;mopuh-name;vdg-firstname; 14160;xbe-city;1970/11/22;vbak-street; 103;MA;MASSACHUSETTS + 839;rifdh-name;eky-firstname; 12900;uwr-city;1978/04/02;yysd-street; 81;GU;GUAM + 840;avlwf-name;xpn-firstname; 14940;ebu-city;1974/08/06;vcki-street; 154;PW;PALAU + 841;avokq-name;dzw-firstname; 14680;xnx-city;1954/02/23;cmis-street; 175;NH;NEW HAMPSHIRE + 842;nahim-name;yvv-firstname; 18020;egx-city;1978/05/05;rpeq-street; 86;NJ;NEW JERSEY + 843;kehud-name;gus-firstname; 13200;bwc-city;1962/02/19;aagh-street; 166;OK;OKLAHOMA + 844;tkfjt-name;kch-firstname; 11160;vuw-city;1974/07/20;mkli-street; 119;WY;WYOMING + 845;gcwwn-name;rpb-firstname; 10640;yum-city;1982/05/17;vqgz-street; 157;OR;OREGON + 846;piobc-name;pii-firstname; 17360;vcs-city;1950/06/02;obsu-street; 105;FM;FEDERATED STATES OF MICRONESIA + 847;nfguz-name;rtb-firstname; 14380;zrh-city;1982/03/01;jltk-street; 164;PW;PALAU + 848;kayxc-name;gew-firstname; 15780;miz-city;1959/01/22;bckb-street; 85;ME;MAINE + 849;kwjrg-name;lwy-firstname; 19080;qym-city;1986/08/26;fagw-street; 181;OR;OREGON + 850;joacl-name;nay-firstname; 15120;wdm-city;1982/12/10;goug-street; 6;VT;VERMONT + 851;fmnme-name;ctx-firstname; 13940;qnj-city;1954/04/12;cacp-street; 155;AL;ALABAMA + 852;cqjtz-name;vqp-firstname; 16480;san-city;1966/10/16;oijg-street; 94;NY;NEW YORK + 853;tdndt-name;qya-firstname; 16320;log-city;1962/05/08;pvtk-street; 151;MP;NORTHERN MARIANA ISLANDS + 854;cquwe-name;lzs-firstname; 12940;vnf-city;1986/08/16;tsfg-street; 181;SD;SOUTH DAKOTA + 855;dbfxb-name;glb-firstname; 18420;ecz-city;1978/11/28;fpuk-street; 42;PW;PALAU + 856;odwcc-name;tcz-firstname; 17420;ffa-city;1954/06/12;kirb-street; 158;WI;WISCONSIN + 857;qctkr-name;wpo-firstname; 16980;upo-city;1978/06/21;gfrm-street; 116;TX;TEXAS + 858;qikks-name;eep-firstname; 18560;tof-city;1986/08/11;trqf-street; 64;WA;WASHINGTON + 859;gbltk-name;jny-firstname; 12080;tdu-city;1978/09/15;wsii-street; 133;ID;IDAHO + 860;cirdu-name;dzw-firstname; 11340;stm-city;1974/04/04;qnat-street; 48;PA;PENNSYLVANIA + 861;xukav-name;rep-firstname; 18240;pzu-city;1950/11/16;piru-street; 24;PW;PALAU + 862;dqxqx-name;eca-firstname; 16080;ddl-city;1982/08/15;snln-street; 109;AK;ALASKA + 863;jxcxv-name;ipw-firstname; 19200;ctv-city;1982/12/03;zazz-street; 171;TN;TENNESSEE + 864;ddgxr-name;bwx-firstname; 18860;giq-city;1954/05/11;ukyp-street; 161;SC;SOUTH CAROLINA + 865;wcboj-name;cqb-firstname; 15080;srk-city;1978/04/28;pycv-street; 12;PW;PALAU + 866;ifgzt-name;bde-firstname; 17700;whw-city;1978/05/05;qfgv-street; 162;SD;SOUTH DAKOTA + 867;riwqf-name;qil-firstname; 18480;mce-city;1990/09/07;qxyc-street; 69;MP;NORTHERN MARIANA ISLANDS + 868;zdudw-name;yxg-firstname; 17380;apv-city;1954/09/09;wymd-street; 195;KS;KANSAS + 869;szvfk-name;nei-firstname; 13660;xqq-city;1974/10/14;ygbr-street; 129;KY;KENTUCKY + 870;lrgna-name;lqv-firstname; 16840;woh-city;1978/09/16;ibzb-street; 87;IL;ILLINOIS + 871;jeaqx-name;kdj-firstname; 16840;lpn-city;1979/01/26;guxb-street; 10;WA;WASHINGTON + 872;ubvth-name;njr-firstname; 14920;hbz-city;1966/10/20;knnm-street; 69;AL;ALABAMA + 873;opkuq-name;sbc-firstname; 10520;wpu-city;1974/10/24;tzoa-street; 10;OK;OKLAHOMA + 874;eeesy-name;sei-firstname; 13420;xhc-city;1978/01/10;wsmi-street; 48;AK;ALASKA + 875;njzrw-name;yep-firstname; 13680;hdj-city;1974/05/12;bhba-street; 42;ME;MAINE + 876;caria-name;jaf-firstname; 18660;scx-city;1962/12/18;envv-street; 158;GU;GUAM + 877;sdwui-name;dax-firstname; 17780;mdr-city;1958/12/22;orah-street; 167;AK;ALASKA + 878;pnklv-name;not-firstname; 11160;gfk-city;1982/08/07;muqk-street; 6;AZ;ARIZONA + 879;xhqwk-name;cgz-firstname; 19940;nji-city;1962/05/06;ihqj-street; 97;TN;TENNESSEE + 880;oigoy-name;ccp-firstname; 14060;ase-city;1958/05/20;faef-street; 133;MD;MARYLAND + 881;nysrz-name;nil-firstname; 17700;ile-city;1966/09/13;aqiy-street; 25;WI;WISCONSIN + 882;wrkvm-name;ftf-firstname; 17780;wim-city;1982/12/12;kzed-street; 57;MN;MINNESOTA + 883;zngvz-name;tcp-firstname; 11640;vba-city;1982/05/29;phle-street; 105;OK;OKLAHOMA + 884;wdubl-name;his-firstname; 19840;ybx-city;1978/08/06;upqg-street; 80;TN;TENNESSEE + 885;fxvpc-name;yxz-firstname; 13380;ocv-city;1958/04/08;shtt-street; 28;IL;ILLINOIS + 886;blbvw-name;nln-firstname; 17740;gwm-city;1955/01/28;ratd-street; 98;UT;UTAH + 887;ajxnx-name;utp-firstname; 15540;eak-city;1950/07/01;zlzg-street; 74;OR;OREGON + 888;aeeid-name;bmc-firstname; 19100;qtd-city;1958/10/28;sgwc-street; 127;OH;OHIO + 889;ypwhc-name;ojx-firstname; 11480;zib-city;1967/01/09;gpfp-street; 76;UT;UTAH + 890;kuxdp-name;exg-firstname; 14840;cvh-city;1986/01/22;jbif-street; 108;TN;TENNESSEE + 891;ujwmw-name;aqz-firstname; 14080;mwz-city;1978/07/22;eggm-street; 8;MN;MINNESOTA + 892;xogup-name;pps-firstname; 11740;bic-city;1970/11/26;thtu-street; 34;ME;MAINE + 893;vmrty-name;rmg-firstname; 11060;cna-city;1970/08/27;uknc-street; 24;GA;GEORGIA + 894;rrwco-name;dji-firstname; 18380;oln-city;1974/01/02;fzqy-street; 129;MI;MICHIGAN + 895;dvcbl-name;nam-firstname; 15040;sbp-city;1962/03/02;txma-street; 170;HI;HAWAII + 896;tbsrx-name;krm-firstname; 15220;gao-city;1959/01/03;fmhf-street; 125;TX;TEXAS + 897;ymjve-name;nmc-firstname; 19280;yux-city;1974/12/11;ycqn-street; 65;FL;FLORIDA + 898;rycyz-name;prd-firstname; 15920;oml-city;1982/09/13;ivie-street; 30;OK;OKLAHOMA + 899;agxhw-name;zla-firstname; 14780;pkw-city;1970/04/28;mcrt-street; 139;MN;MINNESOTA + 900;bbdii-name;wkd-firstname; 15220;eup-city;1962/05/01;neqn-street; 191;ID;IDAHO + 901;svszz-name;gyt-firstname; 12900;sav-city;1954/09/30;gqnm-street; 117;SC;SOUTH CAROLINA + 902;dqidv-name;ooi-firstname; 17340;krq-city;1978/06/24;hjtm-street; 106;NJ;NEW JERSEY + 903;blcfn-name;lrz-firstname; 19240;sav-city;1978/03/01;xrfr-street; 8;RI;RHODE ISLAND + 904;acgjp-name;ncp-firstname; 17900;snk-city;1974/10/27;qbrs-street; 65;IA;IOWA + 905;eloeo-name;tmh-firstname; 17500;use-city;1971/01/05;hkrt-street; 23;PA;PENNSYLVANIA + 906;lxogw-name;hlp-firstname; 10960;yhi-city;1990/10/25;ewnk-street; 7;GU;GUAM + 907;gvwtl-name;dmy-firstname; 19920;fqc-city;1970/05/02;mpze-street; 63;LA;LOUISIANA + 908;vrrxx-name;xmg-firstname; 17160;yks-city;1978/08/14;hikr-street; 165;PW;PALAU + 909;hkpuu-name;ofx-firstname; 17040;hid-city;1978/02/03;rkrn-street; 8;UT;UTAH + 910;bupib-name;ftw-firstname; 16620;fsw-city;1971/01/09;uzxm-street; 95;RI;RHODE ISLAND + 911;siwci-name;fir-firstname; 11180;sqp-city;1974/06/27;pgeq-street; 134;AS;AMERICAN SAMOA + 912;mpkrp-name;etl-firstname; 10560;kty-city;1958/06/02;cpug-street; 67;CA;CALIFORNIA + 913;itfnp-name;ncv-firstname; 16440;dgw-city;1958/07/29;xdkl-street; 131;OK;OKLAHOMA + 914;oyvqm-name;lwu-firstname; 14760;dzo-city;1958/07/09;ubqh-street; 57;IA;IOWA + 915;kuila-name;umo-firstname; 11880;rhy-city;1982/09/17;jwha-street; 81;AK;ALASKA + 916;zduzo-name;gmh-firstname; 12360;oxv-city;1990/06/26;dbxf-street; 76;VI;VIRGIN ISLANDS + 917;cxggf-name;fld-firstname; 12900;sif-city;1954/09/08;wdis-street; 142;WI;WISCONSIN + 918;euelo-name;sbi-firstname; 19340;shq-city;1986/04/20;qmja-street; 3;GA;GEORGIA + 919;papgo-name;oyu-firstname; 10280;bet-city;1970/06/19;otzt-street; 155;CA;CALIFORNIA + 920;dtjeu-name;gvg-firstname; 11980;aie-city;1954/10/17;tcpt-street; 77;NY;NEW YORK + 921;xryzl-name;oqb-firstname; 16540;era-city;1962/08/27;aywk-street; 118;VI;VIRGIN ISLANDS + 922;pydqj-name;rwh-firstname; 15640;ekn-city;1978/09/16;yecx-street; 30;WA;WASHINGTON + 923;picbf-name;cru-firstname; 16040;iaa-city;1986/07/01;ttsg-street; 112;MO;MISSOURI + 924;jfqgk-name;ghq-firstname; 10420;yap-city;1958/06/19;qzbd-street; 1;NV;NEVADA + 925;iqoex-name;dha-firstname; 17340;fqq-city;1966/05/09;mbkz-street; 151;VT;VERMONT + 926;xlugu-name;fae-firstname; 12060;uvm-city;1954/11/21;dhin-street; 100;CT;CONNECTICUT + 927;pxbfc-name;cia-firstname; 15160;xrr-city;1986/06/20;psch-street; 122;MT;MONTANA + 928;koqls-name;teq-firstname; 16100;vtl-city;1982/04/28;qfxy-street; 11;ME;MAINE + 929;nrhco-name;cwj-firstname; 17380;zzw-city;1958/06/19;dcir-street; 188;WA;WASHINGTON + 930;rrxbl-name;gfc-firstname; 13160;ruo-city;1982/06/02;yers-street; 108;MH;MARSHALL ISLANDS + 931;rqtri-name;oqi-firstname; 19780;jvk-city;1970/12/08;dfnt-street; 196;IA;IOWA + 932;txjky-name;gvy-firstname; 19040;jwu-city;1982/03/01;pacb-street; 83;UT;UTAH + 933;efrbq-name;tgu-firstname; 13200;xgk-city;1970/04/10;ipjk-street; 156;WI;WISCONSIN + 934;xzrxi-name;lwt-firstname; 16980;zll-city;1982/01/27;geic-street; 42;LA;LOUISIANA + 935;uafzk-name;wox-firstname; 10240;agj-city;1978/07/03;wdwx-street; 13;TX;TEXAS + 936;npomk-name;upa-firstname; 19060;oad-city;1986/06/27;qkua-street; 200;MP;NORTHERN MARIANA ISLANDS + 937;bepdw-name;box-firstname; 13280;sxr-city;1978/04/15;rnub-street; 19;ME;MAINE + 938;qbbcf-name;igs-firstname; 15440;xrs-city;1958/09/13;yhum-street; 176;MA;MASSACHUSETTS + 939;hirau-name;vyi-firstname; 15980;iyv-city;1962/09/17;gqqy-street; 48;FM;FEDERATED STATES OF MICRONESIA + 940;fzpiq-name;rco-firstname; 10660;wpc-city;1978/03/30;kktg-street; 149;NY;NEW YORK + 941;kvtgo-name;wkl-firstname; 13840;yik-city;1962/02/18;goyb-street; 15;NV;NEVADA + 942;mpljt-name;xyx-firstname; 14880;kdx-city;1982/05/25;fkgb-street; 50;WV;WEST VIRGINIA + 943;cxjtz-name;zkx-firstname; 18500;mrg-city;1974/09/04;seup-street; 139;MP;NORTHERN MARIANA ISLANDS + 944;cbkvb-name;uji-firstname; 13260;rtg-city;1954/02/08;oedv-street; 5;AZ;ARIZONA + 945;qsujg-name;hig-firstname; 11980;obk-city;1974/12/24;awap-street; 140;WI;WISCONSIN + 946;faqty-name;wml-firstname; 19180;qmg-city;1958/07/06;zxlq-street; 9;NV;NEVADA + 947;cdwge-name;aeu-firstname; 16940;axh-city;1970/12/16;nkhw-street; 165;RI;RHODE ISLAND + 948;ctmij-name;tqq-firstname; 12600;apv-city;1990/12/10;egyw-street; 94;MD;MARYLAND + 949;axcvk-name;dhn-firstname; 11900;osf-city;1982/03/17;tzwx-street; 71;OH;OHIO + 950;flhxd-name;tty-firstname; 10380;nfn-city;1970/04/16;iweq-street; 144;MN;MINNESOTA + 951;bnnjj-name;ttq-firstname; 11360;shi-city;1966/11/16;xhde-street; 125;MI;MICHIGAN + 952;ohowr-name;rlf-firstname; 16380;dzs-city;1982/05/12;yiwv-street; 46;IL;ILLINOIS + 953;ofqhs-name;pyy-firstname; 12100;pfy-city;1958/04/28;xqtd-street; 148;UT;UTAH + 954;quyhm-name;oln-firstname; 12140;lqe-city;1978/09/28;prlu-street; 151;MO;MISSOURI + 955;ailpr-name;cut-firstname; 13200;yqu-city;1950/07/05;fxsk-street; 101;IA;IOWA + 956;zeyok-name;vhb-firstname; 17240;nih-city;1986/11/30;bvms-street; 86;WA;WASHINGTON + 957;xrfau-name;owh-firstname; 17180;tep-city;1954/03/02;qcmo-street; 45;NJ;NEW JERSEY + 958;nuoiw-name;rxs-firstname; 11320;uef-city;1962/04/23;oiuf-street; 117;MA;MASSACHUSETTS + 959;imjdp-name;mlh-firstname; 19020;mpi-city;1962/06/28;obxd-street; 11;PA;PENNSYLVANIA + 960;ynmgr-name;uzp-firstname; 15500;oxs-city;1986/07/15;epkx-street; 92;GA;GEORGIA + 961;wcucp-name;qlt-firstname; 12460;sow-city;1986/05/17;qoxd-street; 40;WV;WEST VIRGINIA + 962;ugiex-name;ebp-firstname; 15640;vgo-city;1974/09/28;ggdc-street; 22;MH;MARSHALL ISLANDS + 963;nsilq-name;vxf-firstname; 19280;zzm-city;1986/03/13;kxrw-street; 68;VA;VIRGINIA + 964;whtok-name;oqs-firstname; 18340;ddf-city;1954/10/13;toaz-street; 100;VT;VERMONT + 965;lbwzb-name;rgg-firstname; 15340;bla-city;1978/12/10;usrd-street; 175;TN;TENNESSEE + 966;lcbat-name;nnd-firstname; 16200;ebl-city;1990/11/20;emud-street; 144;MI;MICHIGAN + 967;alouv-name;omr-firstname; 14980;ouw-city;1958/06/11;xbnb-street; 90;AZ;ARIZONA + 968;nvqwv-name;blm-firstname; 17880;nip-city;1974/12/14;sgzi-street; 33;AS;AMERICAN SAMOA + 969;wdidq-name;qwd-firstname; 15480;eis-city;1979/01/22;qqbv-street; 43;NH;NEW HAMPSHIRE + 970;jmlfw-name;gal-firstname; 12680;fjt-city;1955/01/10;iqnm-street; 81;GU;GUAM + 971;feoxv-name;mme-firstname; 18020;oed-city;1986/01/13;jtud-street; 73;NV;NEVADA + 972;gsdoz-name;oxc-firstname; 10420;jff-city;1978/02/20;jnom-street; 62;OR;OREGON + 973;hklot-name;rur-firstname; 13900;nur-city;1986/06/05;nkmx-street; 195;SD;SOUTH DAKOTA + 974;smemp-name;ehc-firstname; 10080;wrv-city;1950/04/10;wfrv-street; 93;FL;FLORIDA + 975;htgft-name;nzy-firstname; 16960;gwe-city;1966/05/09;abiz-street; 58;UT;UTAH + 976;jaikt-name;tls-firstname; 17280;whv-city;1966/09/19;wfzd-street; 70;ME;MAINE + 977;yshns-name;zhh-firstname; 13200;dty-city;1978/12/13;hiht-street; 158;KS;KANSAS + 978;selvf-name;zcr-firstname; 12900;phm-city;1958/12/28;pont-street; 149;GU;GUAM + 979;qoxyo-name;oth-firstname; 10280;uic-city;1986/03/16;havi-street; 153;ID;IDAHO + 980;uabxl-name;cid-firstname; 18000;cea-city;1982/02/18;jykd-street; 127;WV;WEST VIRGINIA + 981;hjnpb-name;wwh-firstname; 12400;fwd-city;1978/10/01;ryqy-street; 101;OH;OHIO + 982;zcqum-name;ecp-firstname; 15820;rpu-city;1958/11/11;fpoi-street; 118;undefined;undefined + 983;cetkk-name;btl-firstname; 18660;xrq-city;1970/04/26;mkuo-street; 167;TX;TEXAS + 984;laofd-name;fqf-firstname; 18180;ddy-city;1986/04/01;qjar-street; 14;OR;OREGON + 985;xjtie-name;tpq-firstname; 16800;fgb-city;1970/03/09;nqpk-street; 108;MA;MASSACHUSETTS + 986;txrwc-name;ygc-firstname; 17880;ipb-city;1962/04/26;zomj-street; 189;DC;DISTRICT OF COLUMBIA + 987;csicd-name;yzm-firstname; 18240;nwu-city;1978/05/03;ishz-street; 24;LA;LOUISIANA + 988;njtnl-name;nxa-firstname; 10360;ols-city;1990/12/01;joem-street; 101;DE;DELAWARE + 989;dclnv-name;hve-firstname; 17080;amp-city;1978/05/22;jfuv-street; 17;WI;WISCONSIN + 990;jrazh-name;yec-firstname; 14000;tsi-city;1986/09/02;guzz-street; 119;KS;KANSAS + 991;hpxko-name;psn-firstname; 11900;ocf-city;1970/04/21;gvbf-street; 12;OK;OKLAHOMA + 992;lggcw-name;sey-firstname; 17260;ike-city;1958/09/19;yrnf-street; 34;NE;NEBRASKA + 993;cxifr-name;agu-firstname; 14360;ujj-city;1954/09/28;widd-street; 127;undefined;undefined + 994;kfwgh-name;qaa-firstname; 17900;kis-city;1970/10/02;wwgu-street; 134;MD;MARYLAND + 995;pcizt-name;fdt-firstname; 15320;tzn-city;1982/09/08;nqii-street; 163;TN;TENNESSEE + 996;hyskd-name;ott-firstname; 19800;zja-city;1962/03/05;tjpp-street; 108;OH;OHIO + 997;nfdtl-name;jxu-firstname; 12980;isx-city;1982/07/05;dldc-street; 166;CA;CALIFORNIA + 998;xtukc-name;aih-firstname; 15360;ctn-city;1974/11/10;qhwy-street; 102;MI;MICHIGAN + 999;qfwcj-name;fdu-firstname; 10100;enp-city;1950/01/11;qqga-street; 19;TX;TEXAS + 1000;uvhph-name;vri-firstname; 11920;tjs-city;1974/04/28;czih-street; 151;NE;NEBRASKA diff --git a/integration-tests/spark-native/files/customers-noheader-1k.txt b/integration-tests/spark-native/files/customers-noheader-1k.txt new file mode 100644 index 00000000000..0870aad9459 --- /dev/null +++ b/integration-tests/spark-native/files/customers-noheader-1k.txt @@ -0,0 +1,1000 @@ + 1;jwcdf-name;fsj-firstname; 13520;oem-city;1954/02/07;amrb-street; 145;AK;ALASKA + 2;flhxu-name;tum-firstname; 17520;buo-city;1966/04/24;wfyz-street; 96;GA;GEORGIA + 3;xthfg-name;gfe-firstname; 12560;vtz-city;1990/01/11;doxx-street; 46;NJ;NEW JERSEY + 4;ulzrz-name;bnl-firstname; 11620;prz-city;1966/08/02;bxqn-street; 104;NY;NEW YORK + 5;oxhyr-name;onx-firstname; 15180;bpn-city;1970/11/14;pksn-street; 133;IN;INDIANA + 6;fiqjz-name;sce-firstname; 16020;fnn-city;1954/09/24;wbhg-street; 35;MD;MARYLAND + 7;tkiat-name;xti-firstname; 12720;stt-city;1966/08/11;tvnf-street; 21;PA;PENNSYLVANIA + 8;kljcz-name;uqd-firstname; 13340;ntt-city;1987/01/15;jyje-street; 10;PW;PALAU + 9;pgunz-name;hcm-firstname; 16680;gxh-city;1970/11/08;shbe-street; 184;NC;NORTH CAROLINA + 10;oyjha-name;uhj-firstname; 18880;uyg-city;1966/04/10;bjgw-street; 176;AR;ARKANSAS + 11;igxbd-name;uph-firstname; 13480;ndh-city;1962/12/03;jdcd-street; 151;NH;NEW HAMPSHIRE + 12;vnaov-name;wha-firstname; 13120;egm-city;1954/03/28;hpep-street; 20;CA;CALIFORNIA + 13;dauuz-name;hwg-firstname; 13740;khn-city;1958/05/15;etqx-street; 5;OK;OKLAHOMA + 14;gkuuo-name;kkb-firstname; 13560;xdt-city;1962/04/07;sdoj-street; 35;MT;MONTANA + 15;wdhze-name;jjk-firstname; 16900;due-city;1970/07/17;pmmu-street; 174;AS;AMERICAN SAMOA + 16;ncayz-name;ynb-firstname; 15720;lxj-city;1974/04/27;mdtb-street; 109;MA;MASSACHUSETTS + 17;rdjin-name;hhu-firstname; 14480;lpc-city;1958/11/16;wxik-street; 145;KY;KENTUCKY + 18;nxzij-name;bdl-firstname; 10740;avx-city;1958/02/20;nybz-street; 138;WI;WISCONSIN + 19;xgrzc-name;dxw-firstname; 18900;vpq-city;1990/11/16;wzjh-street; 58;ME;MAINE + 20;ehgrn-name;vbe-firstname; 17500;cik-city;1978/05/21;ucnw-street; 135;MD;MARYLAND + 21;gctjx-name;upx-firstname; 11960;yqr-city;1958/03/03;rlko-street; 141;TN;TENNESSEE + 22;ptzmg-name;hva-firstname; 15740;gux-city;1978/05/04;pugy-street; 122;VI;VIRGIN ISLANDS + 23;eyeti-name;gnw-firstname; 17420;eko-city;1962/10/26;ylph-street; 61;NC;NORTH CAROLINA + 24;wccwo-name;zpj-firstname; 16600;uim-city;1962/09/29;ygih-street; 26;WA;WASHINGTON + 25;bwkoe-name;ayl-firstname; 18660;rtw-city;1978/07/16;mzww-street; 179;CA;CALIFORNIA + 26;rezku-name;zio-firstname; 19080;nvt-city;1982/07/14;wwkd-street; 91;CA;CALIFORNIA + 27;mjlsk-name;ecx-firstname; 10800;yxu-city;1950/12/11;vttb-street; 195;MO;MISSOURI + 28;wdjsi-name;aoq-firstname; 13660;smo-city;1954/02/01;kako-street; 7;NV;NEVADA + 29;mwfnd-name;nyb-firstname; 19760;bbu-city;1986/09/23;apdi-street; 91;MS;MISSISSIPPI + 30;vtuoz-name;jhh-firstname; 17620;vad-city;1982/05/05;kzup-street; 79;GA;GEORGIA + 31;rhhxk-name;ndr-firstname; 16760;fub-city;1978/11/12;regd-street; 55;OK;OKLAHOMA + 32;lpstk-name;mqz-firstname; 18940;tnr-city;1982/09/16;cdhf-street; 4;SD;SOUTH DAKOTA + 33;ldhyr-name;yts-firstname; 12000;auk-city;1986/11/14;abph-street; 147;IN;INDIANA + 34;cjdml-name;iti-firstname; 16900;wkq-city;1970/06/05;npow-street; 96;NH;NEW HAMPSHIRE + 35;cpenz-name;sbi-firstname; 16380;ssl-city;1962/08/19;kilz-street; 44;MS;MISSISSIPPI + 36;rxtbg-name;anr-firstname; 14720;bqc-city;1958/08/10;pudg-street; 140;NV;NEVADA + 37;udblf-name;raa-firstname; 11500;wli-city;1978/12/13;xomd-street; 41;PW;PALAU + 38;vvyce-name;gep-firstname; 13740;gtd-city;1982/05/23;kwbv-street; 123;undefined;undefined + 39;kwfnz-name;ucu-firstname; 10580;sns-city;1978/08/18;nnun-street; 20;OK;OKLAHOMA + 40;zxydx-name;tml-firstname; 14680;jda-city;1974/05/29;wfjn-street; 157;DC;DISTRICT OF COLUMBIA + 41;bfscx-name;jnl-firstname; 16920;yyg-city;1970/11/30;cgfh-street; 178;CO;COLORADO + 42;qitur-name;yra-firstname; 15560;ijp-city;1978/01/30;fonc-street; 155;AK;ALASKA + 43;msixi-name;ynb-firstname; 12720;ksl-city;1958/07/17;zpjw-street; 46;VI;VIRGIN ISLANDS + 44;wzkjq-name;rgh-firstname; 19000;hkm-city;1974/08/12;yixf-street; 134;CA;CALIFORNIA + 45;dqfmf-name;yxr-firstname; 13840;vie-city;1962/10/23;stvx-street; 39;TX;TEXAS + 46;biluz-name;uqe-firstname; 17760;wkq-city;1962/07/27;embn-street; 183;PW;PALAU + 47;wahfx-name;zwd-firstname; 13240;vic-city;1974/03/27;axpw-street; 131;UT;UTAH + 48;denwt-name;bta-firstname; 17300;hhj-city;1986/12/20;orwy-street; 11;WV;WEST VIRGINIA + 49;akdmy-name;ybz-firstname; 14560;wtx-city;1962/11/08;nwba-street; 123;MP;NORTHERN MARIANA ISLANDS + 50;hqafg-name;nht-firstname; 16080;gfu-city;1951/01/12;spsq-street; 45;LA;LOUISIANA + 51;zhmbl-name;lnw-firstname; 17460;hse-city;1986/12/21;scis-street; 97;GA;GEORGIA + 52;snwnj-name;jyy-firstname; 16400;hsz-city;1966/02/15;imhl-street; 42;NC;NORTH CAROLINA + 53;fuyla-name;mmp-firstname; 11840;hgu-city;1986/08/16;ixiz-street; 145;NC;NORTH CAROLINA + 54;yvfqz-name;prz-firstname; 11260;wjl-city;1982/05/06;fbzd-street; 97;MO;MISSOURI + 55;usbgq-name;vhd-firstname; 14080;dsb-city;1958/04/01;ggoc-street; 54;KS;KANSAS + 56;yaeni-name;zpy-firstname; 19100;sen-city;1954/12/10;sbsw-street; 158;HI;HAWAII + 57;fgxvr-name;vzi-firstname; 17520;lcf-city;1958/11/01;nbdv-street; 10;GU;GUAM + 58;tqpbq-name;rwr-firstname; 19140;zpd-city;1978/08/23;npvb-street; 190;DC;DISTRICT OF COLUMBIA + 59;ieigg-name;ayq-firstname; 12960;ljc-city;1962/07/05;dnjz-street; 163;FL;FLORIDA + 60;rfvzu-name;edm-firstname; 13340;kvz-city;1954/12/08;eijd-street; 4;RI;RHODE ISLAND + 61;pduwm-name;gqb-firstname; 14240;cyr-city;1954/07/03;ndux-street; 13;SD;SOUTH DAKOTA + 62;yyixf-name;yzt-firstname; 18020;lwx-city;1974/01/29;iede-street; 120;NV;NEVADA + 63;dkszq-name;ytd-firstname; 14700;zwh-city;1979/01/11;nbjz-street; 65;AS;AMERICAN SAMOA + 64;slkzv-name;zbg-firstname; 19880;oee-city;1978/11/01;sphg-street; 119;OK;OKLAHOMA + 65;nvxim-name;phc-firstname; 19220;vgg-city;1991/01/24;juok-street; 106;FM;FEDERATED STATES OF MICRONESIA + 66;piyfg-name;xtn-firstname; 13760;nde-city;1954/07/22;vfrv-street; 11;NY;NEW YORK + 67;jnusz-name;mjw-firstname; 12640;nwb-city;1986/08/23;kcsa-street; 138;VA;VIRGINIA + 68;jnypj-name;ioq-firstname; 17000;zqy-city;1986/01/09;croe-street; 119;PW;PALAU + 69;uohts-name;btx-firstname; 13480;dal-city;1990/10/22;llyw-street; 150;WA;WASHINGTON + 70;aavpj-name;pvw-firstname; 13780;lai-city;1954/09/23;nygu-street; 171;FL;FLORIDA + 71;nbjcj-name;rsf-firstname; 12000;kjl-city;1986/06/30;ijsb-street; 123;ID;IDAHO + 72;syjxh-name;gkq-firstname; 19960;rmd-city;1978/10/26;qmyp-street; 161;MN;MINNESOTA + 73;vkojz-name;ryo-firstname; 14300;bmz-city;1954/09/11;gcpj-street; 71;ND;NORTH DAKOTA + 74;pqzfw-name;kld-firstname; 16400;qvq-city;1962/09/09;dhbv-street; 92;ND;NORTH DAKOTA + 75;owvjk-name;fez-firstname; 19740;ldb-city;1978/06/14;kabf-street; 87;VA;VIRGINIA + 76;qsfih-name;ixe-firstname; 16860;qvr-city;1987/01/07;qean-street; 159;CO;COLORADO + 77;slixq-name;gmb-firstname; 19980;ftt-city;1982/06/22;xinx-street; 111;VT;VERMONT + 78;eegsa-name;xlc-firstname; 12680;byk-city;1954/04/23;beul-street; 56;MD;MARYLAND + 79;phevp-name;ihs-firstname; 16120;adc-city;1978/04/25;voig-street; 98;NM;NEW MEXICO + 80;njfoe-name;tag-firstname; 16580;tnr-city;1966/12/04;dhky-street; 108;LA;LOUISIANA + 81;bdncx-name;hcd-firstname; 11260;xcl-city;1970/07/02;jvlp-street; 49;GA;GEORGIA + 82;ikedo-name;tks-firstname; 17460;odl-city;1958/08/25;iaaq-street; 8;GU;GUAM + 83;iafxy-name;vur-firstname; 11480;hgt-city;1962/08/03;hmec-street; 164;TX;TEXAS + 84;lafhf-name;ssz-firstname; 19560;wwp-city;1951/01/25;mxmq-street; 96;IN;INDIANA + 85;okyny-name;hbu-firstname; 16800;yok-city;1978/03/28;ipjz-street; 135;NV;NEVADA + 86;hznby-name;fwy-firstname; 13680;wbi-city;1970/07/25;mxui-street; 170;CT;CONNECTICUT + 87;ztpoa-name;rzk-firstname; 18500;qum-city;1970/07/26;blqr-street; 152;ME;MAINE + 88;gitxz-name;axt-firstname; 11800;fck-city;1974/01/12;tmjw-street; 189;SD;SOUTH DAKOTA + 89;ziomm-name;mcv-firstname; 12940;iwq-city;1950/10/22;hqgj-street; 140;DC;DISTRICT OF COLUMBIA + 90;otncg-name;tuy-firstname; 16540;ulk-city;1971/01/24;yuia-street; 166;TX;TEXAS + 91;cnabb-name;hoq-firstname; 16300;tuw-city;1962/06/17;ujvv-street; 61;ME;MAINE + 92;ucogf-name;ggc-firstname; 14500;fsj-city;1978/02/08;asfi-street; 53;WV;WEST VIRGINIA + 93;lbpmf-name;sdt-firstname; 10780;ewj-city;1978/03/08;hxsp-street; 102;NV;NEVADA + 94;tieqq-name;uyu-firstname; 17740;wea-city;1966/10/31;abpl-street; 187;MO;MISSOURI + 95;fsgwf-name;vjd-firstname; 12460;ads-city;1970/11/29;yeou-street; 10;MA;MASSACHUSETTS + 96;reeba-name;kzs-firstname; 13100;zhc-city;1966/07/08;abmv-street; 88;FL;FLORIDA + 97;shybc-name;gcp-firstname; 10660;ahg-city;1950/12/15;hrqy-street; 174;KS;KANSAS + 98;phszr-name;sst-firstname; 13080;ydd-city;1954/09/23;quqn-street; 2;RI;RHODE ISLAND + 99;jteco-name;fxc-firstname; 19760;agr-city;1986/05/06;dzxc-street; 108;MD;MARYLAND + 100;qvaar-name;icx-firstname; 16120;boc-city;1978/08/04;bfzf-street; 12;NM;NEW MEXICO + 101;iiapu-name;veo-firstname; 10180;wdv-city;1954/05/29;ovyu-street; 55;WI;WISCONSIN + 102;qfawh-name;wlx-firstname; 11280;fad-city;1962/04/28;hibx-street; 188;MP;NORTHERN MARIANA ISLANDS + 103;nfurq-name;rib-firstname; 17080;xcp-city;1962/11/10;rqui-street; 164;MI;MICHIGAN + 104;hdbiw-name;wxm-firstname; 12600;txy-city;1978/11/23;yfcx-street; 112;OK;OKLAHOMA + 105;oyher-name;jws-firstname; 17900;bai-city;1978/12/30;tyil-street; 178;NC;NORTH CAROLINA + 106;fzjnb-name;wxk-firstname; 12300;fda-city;1974/03/26;aweg-street; 7;MO;MISSOURI + 107;ycpcp-name;xfq-firstname; 12660;mna-city;1986/02/14;dcki-street; 5;AZ;ARIZONA + 108;rxxeb-name;qdw-firstname; 17600;yks-city;1970/11/15;zvsf-street; 74;MH;MARSHALL ISLANDS + 109;ffbgl-name;fqf-firstname; 11680;npo-city;1974/12/07;vvan-street; 175;GA;GEORGIA + 110;foygy-name;vog-firstname; 16920;mun-city;1970/07/03;urct-street; 153;HI;HAWAII + 111;imoqe-name;xfe-firstname; 14620;gfv-city;1986/02/20;nlak-street; 181;AK;ALASKA + 112;mnhwk-name;bbt-firstname; 16180;bnf-city;1986/10/10;yybd-street; 144;AL;ALABAMA + 113;lkfyf-name;xhg-firstname; 13260;myb-city;1958/10/27;jjcn-street; 128;GA;GEORGIA + 114;bkhya-name;hrh-firstname; 11500;byw-city;1990/03/03;oubr-street; 41;MH;MARSHALL ISLANDS + 115;avceb-name;ztu-firstname; 10020;ogo-city;1974/05/26;iuob-street; 31;KS;KANSAS + 116;pnacg-name;iws-firstname; 11640;dtx-city;1966/06/01;kwwj-street; 177;WV;WEST VIRGINIA + 117;ypnsc-name;tyv-firstname; 16820;glg-city;1962/12/19;nysz-street; 13;OK;OKLAHOMA + 118;agndn-name;qae-firstname; 15540;tpg-city;1990/05/15;ygzx-street; 166;NV;NEVADA + 119;qqhon-name;tlb-firstname; 19880;iif-city;1982/02/05;vsbj-street; 191;MP;NORTHERN MARIANA ISLANDS + 120;qzxic-name;mot-firstname; 14840;qvf-city;1970/03/19;gelo-street; 149;WA;WASHINGTON + 121;lunca-name;ced-firstname; 13700;wjp-city;1979/01/06;racn-street; 54;ID;IDAHO + 122;jsxhg-name;yoo-firstname; 13440;ulf-city;1982/06/28;krry-street; 55;NY;NEW YORK + 123;ugbci-name;vht-firstname; 17100;ffc-city;1962/10/16;ixha-street; 99;VT;VERMONT + 124;hgtkz-name;xgg-firstname; 17500;qck-city;1978/07/17;xkez-street; 60;AK;ALASKA + 125;ejdoc-name;ovv-firstname; 14920;ocs-city;1954/11/15;bwhd-street; 169;FL;FLORIDA + 126;zoucr-name;ivo-firstname; 14040;grt-city;1990/04/29;qhox-street; 90;WI;WISCONSIN + 127;asofx-name;yzv-firstname; 19600;ixo-city;1982/07/09;slmy-street; 198;MO;MISSOURI + 128;tgowi-name;zvm-firstname; 17560;avv-city;1986/11/04;qnyp-street; 83;AS;AMERICAN SAMOA + 129;toeur-name;ydp-firstname; 17260;dpz-city;1962/11/02;atag-street; 60;IN;INDIANA + 130;eohex-name;vfp-firstname; 17000;gzc-city;1970/01/05;cvlk-street; 188;UT;UTAH + 131;mahci-name;cwt-firstname; 18240;wut-city;1982/08/02;zyse-street; 147;OR;OREGON + 132;hzetq-name;kif-firstname; 14280;aep-city;1990/11/26;rrsr-street; 118;NE;NEBRASKA + 133;tjrgp-name;vle-firstname; 15620;sdv-city;1962/12/08;zdyx-street; 88;WV;WEST VIRGINIA + 134;japyi-name;jkm-firstname; 14960;jeo-city;1958/03/01;bsxv-street; 86;TX;TEXAS + 135;eqley-name;ttv-firstname; 12740;trs-city;1974/09/16;ibff-street; 187;CA;CALIFORNIA + 136;dbvkz-name;efr-firstname; 13700;ujo-city;1958/05/14;louh-street; 22;MP;NORTHERN MARIANA ISLANDS + 137;mpgcz-name;ysk-firstname; 11860;eva-city;1978/08/23;nedx-street; 101;MT;MONTANA + 138;bntsw-name;osn-firstname; 15700;mmv-city;1966/07/28;loqs-street; 34;WY;WYOMING + 139;yfcbv-name;ing-firstname; 10300;ddb-city;1966/04/12;amuj-street; 32;SD;SOUTH DAKOTA + 140;ddwqy-name;rxo-firstname; 18720;nsh-city;1974/06/22;rugn-street; 105;LA;LOUISIANA + 141;zuxwq-name;xha-firstname; 12240;jii-city;1974/04/22;kawh-street; 97;NJ;NEW JERSEY + 142;aarej-name;dfg-firstname; 14680;exu-city;1958/04/17;adua-street; 11;NE;NEBRASKA + 143;iezfw-name;ufb-firstname; 18800;fyv-city;1970/06/05;yvao-street; 53;HI;HAWAII + 144;vvmzr-name;bud-firstname; 15120;ggo-city;1966/07/24;ozcj-street; 127;MT;MONTANA + 145;bknbv-name;qrd-firstname; 11500;mth-city;1970/04/16;ijle-street; 143;NH;NEW HAMPSHIRE + 146;bwyxl-name;fdq-firstname; 13160;ngn-city;1954/07/05;nkco-street; 120;DE;DELAWARE + 147;lkkwb-name;yqh-firstname; 19580;pwn-city;1954/10/16;rgdl-street; 185;MN;MINNESOTA + 148;uokzd-name;aco-firstname; 13940;wyf-city;1966/02/07;lbhd-street; 23;NH;NEW HAMPSHIRE + 149;tdmol-name;hkb-firstname; 11960;wbi-city;1970/06/03;wboh-street; 59;ND;NORTH DAKOTA + 150;erulk-name;xcd-firstname; 11420;kzt-city;1990/02/07;bmcb-street; 160;DC;DISTRICT OF COLUMBIA + 151;atrip-name;mlq-firstname; 14440;agk-city;1986/11/08;qhdv-street; 29;IN;INDIANA + 152;atiir-name;brc-firstname; 11380;sas-city;1958/10/20;dwyv-street; 100;AR;ARKANSAS + 153;cvygb-name;kdu-firstname; 17300;etl-city;1954/11/13;bdxo-street; 43;MA;MASSACHUSETTS + 154;jeyeq-name;yjl-firstname; 12740;jgr-city;1978/06/21;aavd-street; 61;NY;NEW YORK + 155;wojgm-name;xdk-firstname; 13340;meq-city;1982/10/20;ttix-street; 61;MT;MONTANA + 156;oktge-name;taf-firstname; 11200;ibx-city;1990/09/05;clbk-street; 70;UT;UTAH + 157;cdrrm-name;dmu-firstname; 11980;bqa-city;1962/06/18;owlk-street; 26;NY;NEW YORK + 158;jyqvl-name;rht-firstname; 11120;qrk-city;1982/04/20;qbrn-street; 55;WY;WYOMING + 159;spfxh-name;oqv-firstname; 14740;gyh-city;1970/07/08;oiin-street; 59;NC;NORTH CAROLINA + 160;sczwy-name;mhg-firstname; 17860;izz-city;1970/08/25;xehg-street; 2;NJ;NEW JERSEY + 161;lklcm-name;rcy-firstname; 11960;ycf-city;1982/07/04;path-street; 179;NJ;NEW JERSEY + 162;jcluy-name;tlk-firstname; 10380;lsi-city;1970/03/17;ugqr-street; 138;NJ;NEW JERSEY + 163;qqdvp-name;hsh-firstname; 18240;bqf-city;1982/01/01;nupe-street; 153;LA;LOUISIANA + 164;rxlox-name;uoi-firstname; 15600;uvd-city;1954/04/03;gjmv-street; 197;MA;MASSACHUSETTS + 165;kjypq-name;wgt-firstname; 14060;yrs-city;1978/06/09;ijks-street; 144;CO;COLORADO + 166;zegdj-name;fpi-firstname; 13380;znp-city;1978/04/26;pdlh-street; 187;KY;KENTUCKY + 167;ihkkq-name;gtk-firstname; 15740;qbg-city;1970/06/18;odsg-street; 95;CO;COLORADO + 168;yhnuk-name;uhh-firstname; 16720;hoo-city;1978/06/20;vrcy-street; 186;KS;KANSAS + 169;ftpvt-name;ufk-firstname; 13600;wat-city;1954/10/16;nxax-street; 112;OR;OREGON + 170;xoiyz-name;xqq-firstname; 14560;kea-city;1986/08/10;bivl-street; 177;MN;MINNESOTA + 171;wfeuq-name;qec-firstname; 16540;obq-city;1950/11/17;keyf-street; 108;UT;UTAH + 172;pfrmg-name;tyi-firstname; 15360;tjx-city;1979/01/30;lyhr-street; 78;NE;NEBRASKA + 173;najqw-name;ldk-firstname; 10220;bci-city;1982/04/01;qxuf-street; 84;MS;MISSISSIPPI + 174;qbqrg-name;zyo-firstname; 13420;cdh-city;1958/06/13;gqst-street; 167;WY;WYOMING + 175;lnaxv-name;zwt-firstname; 14740;lok-city;1962/10/06;mmdu-street; 149;KY;KENTUCKY + 176;tpgpm-name;qie-firstname; 14960;opy-city;1958/07/14;uxfv-street; 158;SC;SOUTH CAROLINA + 177;nltvw-name;ahc-firstname; 19520;uxf-city;1958/03/16;fwsy-street; 131;CA;CALIFORNIA + 178;ujfpc-name;cwd-firstname; 13800;gki-city;1974/10/10;sgiq-street; 12;FL;FLORIDA + 179;pehcm-name;mah-firstname; 15940;azs-city;1970/05/07;hvvk-street; 9;PR;PUERTO RICO + 180;phlqr-name;qog-firstname; 12160;qvt-city;1966/09/11;isol-street; 155;AZ;ARIZONA + 181;bxerk-name;kxv-firstname; 14180;sek-city;1982/02/18;ctwu-street; 84;CA;CALIFORNIA + 182;nmlqw-name;oyf-firstname; 12640;tmv-city;1962/02/26;eqss-street; 141;NE;NEBRASKA + 183;kobcl-name;pht-firstname; 15820;nky-city;1978/05/14;vfnd-street; 176;PR;PUERTO RICO + 184;lvbqi-name;juh-firstname; 12780;rst-city;1958/12/18;wwko-street; 22;MP;NORTHERN MARIANA ISLANDS + 185;yqqmt-name;zrg-firstname; 12780;hxs-city;1954/08/12;mdxh-street; 190;MS;MISSISSIPPI + 186;osbyt-name;qtk-firstname; 14900;ltd-city;1990/06/21;quqn-street; 59;MO;MISSOURI + 187;vibab-name;vgy-firstname; 19600;jxa-city;1958/09/20;czps-street; 137;AR;ARKANSAS + 188;vrnml-name;qmd-firstname; 15860;mxe-city;1966/07/23;ybfc-street; 148;DE;DELAWARE + 189;thimt-name;ige-firstname; 12900;dqn-city;1966/05/07;bccw-street; 187;OK;OKLAHOMA + 190;jdzou-name;qnd-firstname; 17600;fzi-city;1958/06/12;ewtx-street; 174;IN;INDIANA + 191;bsvvw-name;hfa-firstname; 14180;kmn-city;1974/09/19;zvdw-street; 13;UT;UTAH + 192;iwbao-name;qur-firstname; 19500;jlk-city;1982/08/08;kllj-street; 113;WA;WASHINGTON + 193;xpxla-name;yzv-firstname; 19020;eze-city;1954/04/22;taku-street; 105;AS;AMERICAN SAMOA + 194;gqugh-name;sdy-firstname; 14360;pwi-city;1974/03/11;qybh-street; 95;KY;KENTUCKY + 195;bueoc-name;sfx-firstname; 10560;xhn-city;1970/08/29;zfin-street; 48;NC;NORTH CAROLINA + 196;fyrln-name;fay-firstname; 10820;qtd-city;1974/11/04;yrtc-street; 120;MH;MARSHALL ISLANDS + 197;zuhli-name;qwr-firstname; 19800;nqp-city;1970/01/21;mdew-street; 8;VA;VIRGINIA + 198;rwplk-name;jkr-firstname; 18080;khf-city;1978/02/28;ihkv-street; 134;MT;MONTANA + 199;ssbzy-name;azn-firstname; 11440;ire-city;1954/11/16;sjou-street; 55;CO;COLORADO + 200;zbhue-name;ces-firstname; 19840;ybc-city;1974/07/17;ktsw-street; 94;ND;NORTH DAKOTA + 201;tcpnt-name;tgk-firstname; 19580;nox-city;1990/02/24;rmst-street; 59;MS;MISSISSIPPI + 202;avrpe-name;aaz-firstname; 14000;anm-city;1950/09/02;ddjz-street; 197;FL;FLORIDA + 203;qemau-name;lbl-firstname; 15620;jkx-city;1962/07/23;kxdn-street; 38;PA;PENNSYLVANIA + 204;xzeqe-name;bjx-firstname; 12960;qiv-city;1958/09/07;yohx-street; 22;VA;VIRGINIA + 205;ufbqh-name;dcm-firstname; 17720;tch-city;1978/10/16;sqis-street; 119;NM;NEW MEXICO + 206;cfwje-name;kng-firstname; 15980;hmf-city;1974/09/23;timl-street; 105;NV;NEVADA + 207;dpswi-name;lzu-firstname; 11020;mby-city;1962/10/14;stnj-street; 143;UT;UTAH + 208;padrh-name;yvj-firstname; 17680;pqc-city;1986/11/28;xmxq-street; 81;GU;GUAM + 209;blcun-name;erh-firstname; 16200;sgc-city;1950/10/10;sqkp-street; 29;PA;PENNSYLVANIA + 210;jxuox-name;ztl-firstname; 13140;hox-city;1962/08/12;vxgj-street; 83;KS;KANSAS + 211;bcfua-name;urk-firstname; 11540;mhn-city;1982/10/09;poor-street; 21;VI;VIRGIN ISLANDS + 212;nmccn-name;nlv-firstname; 11780;dec-city;1974/07/05;txyt-street; 125;FL;FLORIDA + 213;cwcfl-name;nye-firstname; 10620;ciu-city;1974/06/14;dumh-street; 124;TX;TEXAS + 214;bewhj-name;mcq-firstname; 16040;vir-city;1951/01/31;uhse-street; 78;MA;MASSACHUSETTS + 215;owbls-name;mcq-firstname; 12940;zjk-city;1962/08/21;plgy-street; 185;NJ;NEW JERSEY + 216;tjwtx-name;uur-firstname; 13400;jqa-city;1962/05/15;hhzi-street; 134;KS;KANSAS + 217;aynmw-name;obp-firstname; 13820;hbu-city;1974/12/09;lfeb-street; 28;NV;NEVADA + 218;aivbc-name;fkc-firstname; 14980;mew-city;1958/05/19;rxqg-street; 41;AK;ALASKA + 219;nkeha-name;ddi-firstname; 17680;wzu-city;1986/03/04;xtik-street; 11;RI;RHODE ISLAND + 220;hyyqm-name;vac-firstname; 17600;gph-city;1954/04/28;rjxi-street; 22;NY;NEW YORK + 221;sifar-name;yth-firstname; 12840;kbe-city;1982/11/18;mnje-street; 5;NY;NEW YORK + 222;bxlyk-name;pla-firstname; 15740;zpb-city;1990/12/07;viys-street; 171;HI;HAWAII + 223;ifcwk-name;yfu-firstname; 10000;itv-city;1982/06/16;iuya-street; 77;SD;SOUTH DAKOTA + 224;fherw-name;acw-firstname; 13000;dxg-city;1970/09/09;zscv-street; 45;VT;VERMONT + 225;cvbfh-name;tbs-firstname; 13160;znt-city;1958/07/20;exfl-street; 171;LA;LOUISIANA + 226;sxpuq-name;qdu-firstname; 13000;lhm-city;1971/01/08;yooq-street; 80;VT;VERMONT + 227;xawor-name;glz-firstname; 18160;dxx-city;1954/12/08;fjnf-street; 130;FM;FEDERATED STATES OF MICRONESIA + 228;dwpda-name;dtg-firstname; 15380;zyz-city;1974/04/21;gozg-street; 96;MT;MONTANA + 229;airyv-name;oue-firstname; 16900;gbm-city;1986/07/14;xfte-street; 45;MP;NORTHERN MARIANA ISLANDS + 230;omfog-name;zhv-firstname; 17020;lep-city;1970/03/21;trww-street; 128;LA;LOUISIANA + 231;ddgah-name;ost-firstname; 13580;ojl-city;1958/03/07;gnln-street; 155;KY;KENTUCKY + 232;feggq-name;cro-firstname; 18780;wtj-city;1966/10/01;jesi-street; 63;NM;NEW MEXICO + 233;ahxvq-name;nes-firstname; 11660;niu-city;1950/06/06;upyk-street; 185;WA;WASHINGTON + 234;gjlqf-name;mvv-firstname; 19620;roc-city;1974/05/01;tsqu-street; 19;MI;MICHIGAN + 235;xbcip-name;vyn-firstname; 10560;nru-city;1986/12/06;qxfi-street; 114;WI;WISCONSIN + 236;cdwuj-name;sks-firstname; 12560;typ-city;1954/01/31;fkwb-street; 128;DC;DISTRICT OF COLUMBIA + 237;bsekx-name;wbw-firstname; 14280;twm-city;1962/09/11;bxui-street; 174;NM;NEW MEXICO + 238;foppd-name;zlw-firstname; 13580;hmg-city;1974/04/29;yiwk-street; 68;MN;MINNESOTA + 239;brtej-name;cqi-firstname; 11000;elz-city;1982/10/16;uauh-street; 23;PA;PENNSYLVANIA + 240;cpklp-name;tps-firstname; 11440;nsm-city;1950/10/28;cmjv-street; 139;PR;PUERTO RICO + 241;opbzn-name;bxz-firstname; 12860;jnq-city;1966/05/08;nkuq-street; 35;MP;NORTHERN MARIANA ISLANDS + 242;kxkmx-name;ziy-firstname; 17460;wqq-city;1974/11/05;gnha-street; 192;WV;WEST VIRGINIA + 243;lxpbu-name;jph-firstname; 19500;fpa-city;1954/10/01;gdls-street; 163;MI;MICHIGAN + 244;erhwd-name;wvu-firstname; 11880;iza-city;1962/08/03;ucsh-street; 75;undefined;undefined + 245;tjuxy-name;jzf-firstname; 10580;nyq-city;1970/04/30;gbes-street; 189;AR;ARKANSAS + 246;piocq-name;skz-firstname; 14600;xuq-city;1978/07/12;inae-street; 27;ME;MAINE + 247;bqjty-name;ybj-firstname; 13040;jqu-city;1954/11/30;ugcn-street; 159;AL;ALABAMA + 248;xexrx-name;fpu-firstname; 19720;ckc-city;1974/06/30;zpez-street; 46;IN;INDIANA + 249;auzgu-name;dam-firstname; 18460;mih-city;1962/07/28;augp-street; 112;MD;MARYLAND + 250;xupoe-name;fdb-firstname; 13440;llr-city;1986/10/01;forq-street; 185;IL;ILLINOIS + 251;sgely-name;pzz-firstname; 15920;jya-city;1950/02/02;kypg-street; 147;DC;DISTRICT OF COLUMBIA + 252;onini-name;zts-firstname; 18060;avs-city;1974/12/02;kxjn-street; 85;TX;TEXAS + 253;tyflk-name;htl-firstname; 17560;bhd-city;1962/06/06;xquf-street; 126;IN;INDIANA + 254;chbez-name;zkj-firstname; 17000;goh-city;1974/03/02;rkui-street; 13;AR;ARKANSAS + 255;zmmhg-name;rqb-firstname; 11340;egt-city;1970/02/06;pwjj-street; 6;MH;MARSHALL ISLANDS + 256;gutfo-name;vki-firstname; 18860;xdv-city;1986/07/11;iwkf-street; 14;DC;DISTRICT OF COLUMBIA + 257;eogwr-name;hnt-firstname; 19840;nht-city;1990/05/02;kljr-street; 18;VT;VERMONT + 258;ibupi-name;ygc-firstname; 18580;tvk-city;1978/08/20;xphm-street; 123;CO;COLORADO + 259;wqpaq-name;uwc-firstname; 10780;ygl-city;1958/03/20;ncta-street; 87;GU;GUAM + 260;swcms-name;ljb-firstname; 13560;pvt-city;1954/12/13;ovsf-street; 176;MD;MARYLAND + 261;xwxmt-name;qpq-firstname; 13380;fzc-city;1978/10/14;ivwb-street; 17;AZ;ARIZONA + 262;drxej-name;msd-firstname; 12720;ure-city;1986/02/15;nrqa-street; 30;MT;MONTANA + 263;scgpq-name;wgg-firstname; 18740;xtx-city;1990/05/09;vxwl-street; 29;AK;ALASKA + 264;qchcm-name;fbc-firstname; 14480;ymf-city;1978/03/21;cfqc-street; 63;ND;NORTH DAKOTA + 265;cbpxm-name;clb-firstname; 16900;etx-city;1974/03/29;uzyw-street; 175;OK;OKLAHOMA + 266;clksy-name;mls-firstname; 13560;qhs-city;1958/04/07;trwx-street; 47;MO;MISSOURI + 267;qwagv-name;hil-firstname; 18240;atx-city;1954/01/30;glaf-street; 57;MT;MONTANA + 268;grkjm-name;qwy-firstname; 15900;jvu-city;1970/02/26;rhdn-street; 172;ME;MAINE + 269;gkiis-name;xhp-firstname; 14440;bgh-city;1954/01/02;btrx-street; 53;FL;FLORIDA + 270;nscdn-name;lfn-firstname; 19900;eph-city;1958/09/29;nqao-street; 171;KS;KANSAS + 271;ulrrc-name;ncb-firstname; 10320;cao-city;1986/10/15;lkrm-street; 125;GA;GEORGIA + 272;qxjcn-name;wpy-firstname; 11260;rew-city;1954/12/19;tldv-street; 115;CA;CALIFORNIA + 273;thyyj-name;htd-firstname; 19100;tae-city;1982/03/04;wbqv-street; 174;PR;PUERTO RICO + 274;uigiq-name;lmx-firstname; 19320;bkr-city;1950/04/07;foio-street; 104;OK;OKLAHOMA + 275;iinbr-name;cfg-firstname; 12100;qwv-city;1986/01/12;ervo-street; 87;MN;MINNESOTA + 276;kipbw-name;fda-firstname; 16300;mlu-city;1970/03/26;yjpd-street; 15;MN;MINNESOTA + 277;cfrgd-name;dui-firstname; 11320;jax-city;1990/04/14;zaik-street; 157;ME;MAINE + 278;duaza-name;xsx-firstname; 11120;kkg-city;1958/02/05;bply-street; 185;VI;VIRGIN ISLANDS + 279;zzhqx-name;vlb-firstname; 18380;vyb-city;1950/12/11;iugj-street; 159;HI;HAWAII + 280;kuryf-name;kib-firstname; 13140;tum-city;1954/05/06;clkw-street; 31;CO;COLORADO + 281;daljt-name;ycr-firstname; 10840;ckw-city;1982/10/29;umof-street; 52;CO;COLORADO + 282;xhssg-name;djc-firstname; 17840;gvj-city;1954/12/12;zgmw-street; 35;WV;WEST VIRGINIA + 283;qqscv-name;eiu-firstname; 15260;daj-city;1986/01/30;qprc-street; 158;MH;MARSHALL ISLANDS + 284;tehbb-name;czj-firstname; 14180;xnh-city;1958/03/06;lfji-street; 48;UT;UTAH + 285;acdsd-name;yiu-firstname; 12220;buk-city;1958/02/15;ovcj-street; 70;ME;MAINE + 286;derpl-name;buv-firstname; 16000;fha-city;1970/09/02;vfay-street; 57;WA;WASHINGTON + 287;lobqd-name;qxn-firstname; 15060;ycp-city;1966/12/24;axxb-street; 38;PA;PENNSYLVANIA + 288;owxja-name;okb-firstname; 15640;lad-city;1982/05/24;wnka-street; 101;MN;MINNESOTA + 289;zohkw-name;ypo-firstname; 15460;wtu-city;1978/08/10;jhco-street; 21;VA;VIRGINIA + 290;nmrar-name;aqm-firstname; 18360;lcn-city;1963/01/26;zxeo-street; 143;WV;WEST VIRGINIA + 291;tndrh-name;ael-firstname; 16360;cyb-city;1958/04/10;cmlg-street; 78;ND;NORTH DAKOTA + 292;kbpkv-name;yck-firstname; 19400;oka-city;1974/11/02;kpmc-street; 10;CO;COLORADO + 293;uabcl-name;zms-firstname; 15940;tzb-city;1970/08/06;ezzs-street; 11;VT;VERMONT + 294;lancp-name;zbk-firstname; 11560;vny-city;1978/07/21;tzys-street; 182;LA;LOUISIANA + 295;wagzv-name;hcp-firstname; 13760;kik-city;1974/11/01;gpuw-street; 151;NH;NEW HAMPSHIRE + 296;qrenr-name;egp-firstname; 16920;zwq-city;1966/03/19;fbwi-street; 102;FL;FLORIDA + 297;amjep-name;mds-firstname; 16700;fvb-city;1978/05/07;peau-street; 167;WV;WEST VIRGINIA + 298;dzppv-name;qav-firstname; 16680;wbc-city;1970/04/15;dzyp-street; 149;VI;VIRGIN ISLANDS + 299;qrlwz-name;hvk-firstname; 14640;qyl-city;1954/03/07;gvsc-street; 32;NC;NORTH CAROLINA + 300;lracx-name;dmp-firstname; 13800;slo-city;1970/09/21;jspb-street; 106;WA;WASHINGTON + 301;jhlao-name;txt-firstname; 12020;jam-city;1962/06/26;bcky-street; 19;WV;WEST VIRGINIA + 302;aocxq-name;mzv-firstname; 17140;dgx-city;1954/08/24;maoi-street; 1;HI;HAWAII + 303;dvdbu-name;fdf-firstname; 10980;goa-city;1982/04/10;onik-street; 109;NM;NEW MEXICO + 304;cqapx-name;skq-firstname; 11080;akw-city;1958/09/02;xakx-street; 14;CA;CALIFORNIA + 305;crmlf-name;djf-firstname; 16100;mle-city;1974/10/10;udrl-street; 96;VT;VERMONT + 306;iujcn-name;tat-firstname; 17660;jnf-city;1966/03/31;inoh-street; 104;TX;TEXAS + 307;aetvh-name;spn-firstname; 14960;wxf-city;1954/06/30;mmxe-street; 78;PA;PENNSYLVANIA + 308;ibfdt-name;sfu-firstname; 11120;kqf-city;1954/06/22;xbwg-street; 132;CA;CALIFORNIA + 309;ccatv-name;aeb-firstname; 10940;qoi-city;1982/08/11;bsrg-street; 29;UT;UTAH + 310;udrjw-name;agc-firstname; 17100;aep-city;1974/04/23;bsju-street; 59;PR;PUERTO RICO + 311;crjcx-name;nbb-firstname; 14820;xtp-city;1958/11/08;fajh-street; 95;WY;WYOMING + 312;qbcrw-name;mef-firstname; 14220;mwa-city;1982/11/13;keyy-street; 97;NE;NEBRASKA + 313;wxvpi-name;dym-firstname; 10220;ncp-city;1954/09/28;qgrg-street; 25;undefined;undefined + 314;bandc-name;hzf-firstname; 18700;eoh-city;1990/11/05;qphi-street; 177;IL;ILLINOIS + 315;afatf-name;eii-firstname; 15480;lsm-city;1982/10/11;nzjq-street; 96;VI;VIRGIN ISLANDS + 316;trxge-name;vhe-firstname; 18260;ccm-city;1958/03/09;ducm-street; 1;NH;NEW HAMPSHIRE + 317;kdygt-name;tgc-firstname; 12500;bqo-city;1978/06/20;rqgx-street; 90;DC;DISTRICT OF COLUMBIA + 318;angdt-name;lvt-firstname; 13960;tbr-city;1974/07/14;wfqj-street; 196;GU;GUAM + 319;hqhqe-name;nxd-firstname; 14860;yhi-city;1954/04/23;wkpi-street; 23;MP;NORTHERN MARIANA ISLANDS + 320;pwwep-name;qtw-firstname; 15160;utn-city;1958/06/08;sheh-street; 176;FL;FLORIDA + 321;vkken-name;wrw-firstname; 16760;jhm-city;1974/12/13;czmf-street; 183;AR;ARKANSAS + 322;zbbmz-name;ipe-firstname; 14340;mco-city;1970/04/25;ymou-street; 2;WY;WYOMING + 323;eyzfr-name;xeb-firstname; 10440;qsa-city;1990/04/15;iukl-street; 135;AZ;ARIZONA + 324;tfpxy-name;yzh-firstname; 14080;wbc-city;1970/02/12;ojfc-street; 150;MP;NORTHERN MARIANA ISLANDS + 325;tphou-name;xac-firstname; 15940;ncy-city;1962/09/22;vcxl-street; 90;AL;ALABAMA + 326;bfscx-name;yih-firstname; 18900;bxa-city;1962/10/12;qsww-street; 137;VT;VERMONT + 327;ussbw-name;gbb-firstname; 15600;wtg-city;1978/07/29;vqcq-street; 10;KY;KENTUCKY + 328;zkoqj-name;mqn-firstname; 13440;lux-city;1970/05/01;bokx-street; 106;VA;VIRGINIA + 329;zbqew-name;dbw-firstname; 14520;hbi-city;1971/01/20;syea-street; 192;CT;CONNECTICUT + 330;vmfuy-name;qge-firstname; 16660;vnk-city;1982/08/29;tlxy-street; 166;PW;PALAU + 331;wajgr-name;mnx-firstname; 17500;wiv-city;1954/07/10;ylug-street; 21;OR;OREGON + 332;lzcry-name;tzk-firstname; 18980;icz-city;1966/06/04;ldvu-street; 25;GU;GUAM + 333;rodpv-name;rix-firstname; 16540;tpf-city;1986/02/03;leur-street; 169;SD;SOUTH DAKOTA + 334;pxmhq-name;tyd-firstname; 11200;okj-city;1978/12/30;tauk-street; 94;ND;NORTH DAKOTA + 335;kansg-name;qzs-firstname; 18020;yhj-city;1982/08/27;ehie-street; 140;AR;ARKANSAS + 336;kpqff-name;bqs-firstname; 13780;tnp-city;1958/08/07;euhw-street; 182;SD;SOUTH DAKOTA + 337;akvdd-name;bxi-firstname; 15620;rbk-city;1962/03/16;fzdy-street; 125;WA;WASHINGTON + 338;nbazl-name;ikv-firstname; 13160;wci-city;1966/08/22;smzb-street; 136;SD;SOUTH DAKOTA + 339;zrowi-name;udo-firstname; 16460;hbe-city;1962/07/07;xgyd-street; 170;AZ;ARIZONA + 340;jfzjd-name;atn-firstname; 13040;yef-city;1974/09/26;dyjj-street; 127;WI;WISCONSIN + 341;prsbo-name;ibh-firstname; 19520;qro-city;1986/01/10;pemg-street; 183;SC;SOUTH CAROLINA + 342;dwfdn-name;kci-firstname; 14640;fze-city;1970/07/24;xxdz-street; 102;IA;IOWA + 343;cnupm-name;rjl-firstname; 14660;ewk-city;1966/07/02;wzjr-street; 107;PW;PALAU + 344;wbpor-name;fuf-firstname; 11300;xne-city;1978/02/14;tyzx-street; 2;AL;ALABAMA + 345;tjqky-name;mjf-firstname; 12560;qlo-city;1982/10/21;hzos-street; 121;NE;NEBRASKA + 346;yxqsn-name;ofx-firstname; 18680;ute-city;1974/02/27;wqka-street; 111;FL;FLORIDA + 347;wjwmv-name;hra-firstname; 14520;mvj-city;1954/10/03;chmz-street; 70;SD;SOUTH DAKOTA + 348;aipnn-name;paa-firstname; 14400;gyq-city;1970/04/27;tbpq-street; 84;undefined;undefined + 349;wplyl-name;zvh-firstname; 15820;vhw-city;1967/01/12;eesj-street; 110;undefined;undefined + 350;syoip-name;lbd-firstname; 13860;wfo-city;1966/03/17;lsvf-street; 47;VI;VIRGIN ISLANDS + 351;lvhdk-name;pzl-firstname; 14320;bab-city;1950/05/01;wktz-street; 134;AR;ARKANSAS + 352;jodya-name;apd-firstname; 14520;nfr-city;1986/12/14;jgzs-street; 193;KY;KENTUCKY + 353;ikunj-name;syx-firstname; 17320;cqd-city;1982/08/19;xwzj-street; 53;IN;INDIANA + 354;bnhud-name;uvd-firstname; 15360;ynw-city;1971/01/25;gens-street; 107;MT;MONTANA + 355;prkrf-name;ivm-firstname; 19220;kie-city;1966/11/14;boem-street; 59;HI;HAWAII + 356;ucywi-name;sjl-firstname; 11480;ish-city;1982/11/13;szck-street; 148;NE;NEBRASKA + 357;sgftd-name;ajp-firstname; 11760;jco-city;1958/03/05;djek-street; 196;UT;UTAH + 358;kohdq-name;phr-firstname; 13120;kpb-city;1978/07/26;csom-street; 17;PR;PUERTO RICO + 359;fgjoa-name;srp-firstname; 11480;onn-city;1990/05/26;duvo-street; 127;MS;MISSISSIPPI + 360;xgiyw-name;rcu-firstname; 15140;mti-city;1958/09/26;lego-street; 125;MA;MASSACHUSETTS + 361;kgcui-name;grq-firstname; 12640;tjn-city;1986/02/08;ntoq-street; 6;CT;CONNECTICUT + 362;ljyxw-name;lho-firstname; 19100;hmd-city;1962/03/09;mqus-street; 12;CT;CONNECTICUT + 363;vnvwy-name;vmv-firstname; 14360;cpu-city;1966/04/20;cfts-street; 16;MI;MICHIGAN + 364;gedwu-name;ocl-firstname; 11760;vaf-city;1970/04/22;exot-street; 121;MT;MONTANA + 365;enknw-name;vjk-firstname; 11100;bge-city;1954/07/06;inhb-street; 95;AS;AMERICAN SAMOA + 366;qmpom-name;tmp-firstname; 12260;ymn-city;1986/07/02;psdj-street; 71;MI;MICHIGAN + 367;jrbwj-name;imr-firstname; 11080;qsx-city;1966/11/08;msys-street; 80;ID;IDAHO + 368;dzjpp-name;cwl-firstname; 15720;ipm-city;1958/07/13;acsb-street; 87;IL;ILLINOIS + 369;qdwdn-name;dtc-firstname; 12740;qab-city;1986/11/29;besk-street; 80;DC;DISTRICT OF COLUMBIA + 370;gbeyp-name;lzl-firstname; 19740;ljz-city;1974/10/01;zwga-street; 52;RI;RHODE ISLAND + 371;kuwva-name;noq-firstname; 17500;xia-city;1950/03/18;omzn-street; 164;KS;KANSAS + 372;jjhsm-name;cdc-firstname; 13020;xli-city;1986/06/10;nups-street; 38;VA;VIRGINIA + 373;xyupl-name;fyd-firstname; 16100;fqd-city;1971/01/05;icjo-street; 28;NE;NEBRASKA + 374;ueipj-name;meb-firstname; 19880;nsk-city;1974/08/15;aqwr-street; 14;LA;LOUISIANA + 375;vdmif-name;fat-firstname; 19120;dud-city;1974/07/01;krgw-street; 178;MS;MISSISSIPPI + 376;oxgdf-name;apn-firstname; 11400;dji-city;1962/04/02;ttnt-street; 13;HI;HAWAII + 377;xgxyc-name;jrn-firstname; 19400;bfg-city;1950/12/30;jzgy-street; 43;WY;WYOMING + 378;bczfk-name;bfu-firstname; 16920;qxp-city;1974/05/26;seja-street; 178;HI;HAWAII + 379;pppqe-name;kwo-firstname; 15260;geb-city;1986/04/06;okvv-street; 11;MN;MINNESOTA + 380;opzyy-name;mfk-firstname; 14960;nlo-city;1962/03/03;dezo-street; 106;AS;AMERICAN SAMOA + 381;xmnsm-name;ckj-firstname; 10400;fnr-city;1950/10/21;uhme-street; 154;NC;NORTH CAROLINA + 382;moqtn-name;zgw-firstname; 12920;pqk-city;1950/10/28;suvi-street; 102;WY;WYOMING + 383;byapu-name;pix-firstname; 10900;gik-city;1982/09/04;ntiq-street; 45;VI;VIRGIN ISLANDS + 384;zuhdb-name;gbj-firstname; 18760;vkk-city;1978/06/17;vnem-street; 62;TX;TEXAS + 385;gkjpo-name;qwq-firstname; 12380;ame-city;1982/12/01;ndvp-street; 94;VA;VIRGINIA + 386;yefwf-name;aev-firstname; 13580;mor-city;1962/07/09;ldcg-street; 91;AL;ALABAMA + 387;twgsp-name;mwx-firstname; 15840;bbp-city;1970/05/12;hecl-street; 137;WY;WYOMING + 388;zatdb-name;tes-firstname; 17080;yga-city;1954/11/05;dfwu-street; 58;undefined;undefined + 389;qxdyx-name;tum-firstname; 18100;mqw-city;1958/10/21;gndl-street; 156;FL;FLORIDA + 390;ncpki-name;wbm-firstname; 13860;cuo-city;1970/07/02;yqpa-street; 30;NE;NEBRASKA + 391;kyzsl-name;djj-firstname; 17400;zzd-city;1978/02/11;mvkp-street; 4;ME;MAINE + 392;jtksy-name;ayp-firstname; 10760;gui-city;1982/12/17;wohf-street; 175;DC;DISTRICT OF COLUMBIA + 393;grzby-name;oyz-firstname; 15140;bmz-city;1974/07/10;rrui-street; 60;NC;NORTH CAROLINA + 394;sgaut-name;xzd-firstname; 15260;dnb-city;1978/10/01;hcii-street; 169;RI;RHODE ISLAND + 395;ozkaq-name;brx-firstname; 11400;guw-city;1962/02/08;pswz-street; 194;NH;NEW HAMPSHIRE + 396;uqivg-name;map-firstname; 10880;lgr-city;1990/07/14;lxmi-street; 143;MN;MINNESOTA + 397;soqkg-name;jws-firstname; 17200;dss-city;1970/12/05;ppht-street; 187;UT;UTAH + 398;pdqdm-name;erh-firstname; 11860;obj-city;1986/04/03;aova-street; 121;CT;CONNECTICUT + 399;ogqrv-name;uyf-firstname; 16400;abs-city;1987/01/06;xwue-street; 114;AZ;ARIZONA + 400;ukcxw-name;ltd-firstname; 11760;bwi-city;1986/09/01;ddjt-street; 199;AR;ARKANSAS + 401;djcgr-name;tet-firstname; 19380;ure-city;1962/09/03;alzp-street; 169;SC;SOUTH CAROLINA + 402;ibbvs-name;cyv-firstname; 16060;gdh-city;1982/04/24;qrdz-street; 116;NV;NEVADA + 403;blmke-name;jtq-firstname; 17260;tls-city;1970/05/28;eylx-street; 171;NV;NEVADA + 404;qpatk-name;mtt-firstname; 13400;nzv-city;1970/03/25;exby-street; 93;WY;WYOMING + 405;mbngf-name;pqp-firstname; 10720;ann-city;1966/09/01;rkpu-street; 146;CT;CONNECTICUT + 406;rcydx-name;usf-firstname; 12880;jou-city;1982/02/20;gtqw-street; 186;MO;MISSOURI + 407;srszq-name;dtb-firstname; 14340;yrs-city;1978/03/16;wtan-street; 89;MN;MINNESOTA + 408;ezlfr-name;ebd-firstname; 19420;ctw-city;1986/03/22;zjyv-street; 44;VT;VERMONT + 409;bhotj-name;hzt-firstname; 16480;rbt-city;1978/06/11;mbsm-street; 157;WV;WEST VIRGINIA + 410;udzwf-name;kfd-firstname; 13440;maj-city;1986/06/06;kerz-street; 180;AZ;ARIZONA + 411;yflfv-name;zdl-firstname; 13300;zix-city;1982/05/20;ozcp-street; 153;MN;MINNESOTA + 412;wlyyq-name;kdz-firstname; 11400;ygb-city;1966/11/07;zekv-street; 111;AS;AMERICAN SAMOA + 413;wonzh-name;eeb-firstname; 19920;qnt-city;1982/09/20;fxob-street; 95;LA;LOUISIANA + 414;eiwzh-name;hpm-firstname; 15860;bfo-city;1967/01/30;tkpg-street; 182;VA;VIRGINIA + 415;mbqgk-name;jsm-firstname; 15040;dai-city;1954/06/12;jqdh-street; 65;NE;NEBRASKA + 416;jhigj-name;qyn-firstname; 18360;ntm-city;1974/05/15;eghd-street; 188;ME;MAINE + 417;llytl-name;jqe-firstname; 11280;kkp-city;1974/04/03;oudg-street; 69;DE;DELAWARE + 418;urhgl-name;iji-firstname; 13760;lhf-city;1962/12/02;oywx-street; 6;SC;SOUTH CAROLINA + 419;uflrm-name;hef-firstname; 15040;usl-city;1974/11/06;qvgi-street; 103;OK;OKLAHOMA + 420;gbfui-name;goz-firstname; 14940;edu-city;1986/02/25;gkqy-street; 26;KS;KANSAS + 421;nysbp-name;fro-firstname; 18420;wqt-city;1958/12/19;vpoc-street; 89;GA;GEORGIA + 422;fmrwo-name;wlf-firstname; 17820;qwb-city;1962/05/03;stcz-street; 22;CA;CALIFORNIA + 423;racwd-name;kqr-firstname; 10180;xdr-city;1974/04/09;fqxz-street; 15;MO;MISSOURI + 424;jpopz-name;krm-firstname; 13420;fjx-city;1970/10/29;kyph-street; 54;NJ;NEW JERSEY + 425;fwdat-name;ppn-firstname; 15660;zqh-city;1986/06/24;zgfe-street; 61;SC;SOUTH CAROLINA + 426;orznz-name;hyy-firstname; 12020;lju-city;1982/07/01;xisy-street; 125;GU;GUAM + 427;spzxo-name;mpv-firstname; 13220;cbq-city;1962/05/09;qlqx-street; 53;OK;OKLAHOMA + 428;qxdra-name;ifp-firstname; 10480;nvu-city;1958/08/15;egsq-street; 133;MH;MARSHALL ISLANDS + 429;yhelu-name;jsc-firstname; 17200;clp-city;1954/12/01;vahx-street; 3;NV;NEVADA + 430;umvfv-name;mbe-firstname; 13600;knp-city;1954/07/16;oldf-street; 188;PA;PENNSYLVANIA + 431;mwcfe-name;xxi-firstname; 15080;chq-city;1974/12/22;kpsj-street; 163;NV;NEVADA + 432;wvkrp-name;dtr-firstname; 19840;pqv-city;1986/07/07;jnzd-street; 119;HI;HAWAII + 433;vqfja-name;kep-firstname; 19380;ydo-city;1966/11/11;gsfd-street; 12;AR;ARKANSAS + 434;ikbjr-name;ipd-firstname; 17920;uld-city;1990/03/03;tmbc-street; 49;KS;KANSAS + 435;hyjrs-name;jqp-firstname; 18820;vvm-city;1970/05/07;txye-street; 152;AR;ARKANSAS + 436;tuewv-name;lkd-firstname; 17060;slo-city;1970/07/06;htdm-street; 197;OK;OKLAHOMA + 437;muyws-name;iql-firstname; 19540;cwf-city;1962/04/27;knsx-street; 167;LA;LOUISIANA + 438;tgsga-name;lww-firstname; 17780;goh-city;1982/12/24;lzcl-street; 136;SD;SOUTH DAKOTA + 439;agsyj-name;fve-firstname; 19260;wgi-city;1970/03/07;aone-street; 48;WV;WEST VIRGINIA + 440;yrgqp-name;tni-firstname; 15820;mzp-city;1986/04/19;femc-street; 29;PR;PUERTO RICO + 441;ltjmw-name;cps-firstname; 13060;aeo-city;1954/07/17;bewv-street; 182;OK;OKLAHOMA + 442;gjkpl-name;tre-firstname; 16340;ndn-city;1962/08/27;ocld-street; 16;WV;WEST VIRGINIA + 443;ryvgz-name;bhe-firstname; 14960;lcg-city;1978/08/09;bxwa-street; 19;NM;NEW MEXICO + 444;zgwwi-name;umb-firstname; 11840;llj-city;1958/07/06;uiww-street; 115;MP;NORTHERN MARIANA ISLANDS + 445;qaczz-name;qng-firstname; 10900;umr-city;1970/01/17;mlsy-street; 6;VA;VIRGINIA + 446;xcruf-name;vbp-firstname; 17840;vzl-city;1982/06/17;oatg-street; 110;MN;MINNESOTA + 447;egqdv-name;boy-firstname; 18360;gfw-city;1958/10/07;qaoh-street; 104;AS;AMERICAN SAMOA + 448;digvt-name;fid-firstname; 11180;iyw-city;1958/03/13;pooo-street; 119;NV;NEVADA + 449;gxavv-name;bal-firstname; 17020;cra-city;1966/02/01;nchf-street; 122;CO;COLORADO + 450;tiwoz-name;vwo-firstname; 19340;oja-city;1982/07/16;bnsy-street; 149;MA;MASSACHUSETTS + 451;jemtu-name;gnk-firstname; 11180;gbb-city;1954/03/01;pmbh-street; 87;NC;NORTH CAROLINA + 452;rhdjl-name;qaf-firstname; 16500;nqr-city;1974/03/28;vmrd-street; 75;DE;DELAWARE + 453;qdkbn-name;cpl-firstname; 17460;ugb-city;1987/01/29;cwoa-street; 184;ME;MAINE + 454;tuhon-name;bbg-firstname; 10500;cer-city;1958/03/02;ttst-street; 184;WA;WASHINGTON + 455;wluxg-name;xpv-firstname; 18240;axq-city;1958/07/28;useg-street; 140;NY;NEW YORK + 456;ojkgh-name;tzq-firstname; 16000;guv-city;1990/11/25;hagy-street; 198;MN;MINNESOTA + 457;arxsx-name;ana-firstname; 18620;oxx-city;1986/04/22;hkdb-street; 28;SD;SOUTH DAKOTA + 458;ujpiw-name;bty-firstname; 14000;kiy-city;1978/08/10;bmgt-street; 176;PW;PALAU + 459;sufuk-name;izq-firstname; 17740;kfy-city;1986/10/29;xxcz-street; 26;MD;MARYLAND + 460;nedie-name;ajz-firstname; 11820;fnf-city;1970/04/08;ccnd-street; 108;MT;MONTANA + 461;vbfwv-name;anp-firstname; 19880;qag-city;1962/11/17;tbcc-street; 23;NJ;NEW JERSEY + 462;bmrzz-name;yfe-firstname; 11300;rgi-city;1970/10/01;xbjp-street; 26;FM;FEDERATED STATES OF MICRONESIA + 463;jewvw-name;ymh-firstname; 13100;kcv-city;1982/03/01;cjbt-street; 4;KY;KENTUCKY + 464;kxxpm-name;has-firstname; 18500;hlp-city;1954/04/03;qsmq-street; 77;MD;MARYLAND + 465;qkjxa-name;gdq-firstname; 12240;qtz-city;1954/11/25;mevz-street; 12;NM;NEW MEXICO + 466;vmdwq-name;vjm-firstname; 19980;rmz-city;1986/09/09;ifxg-street; 139;NC;NORTH CAROLINA + 467;ssgil-name;lkd-firstname; 14220;ndd-city;1963/01/19;rjln-street; 195;MD;MARYLAND + 468;szbhe-name;pwi-firstname; 10100;iij-city;1966/04/09;mojp-street; 177;AK;ALASKA + 469;eswrp-name;lts-firstname; 10080;cuk-city;1966/03/29;plor-street; 139;VI;VIRGIN ISLANDS + 470;tioeh-name;qgc-firstname; 11800;zre-city;1954/06/05;owaq-street; 98;LA;LOUISIANA + 471;fkzpf-name;bse-firstname; 19040;cor-city;1962/06/21;aamy-street; 53;NY;NEW YORK + 472;kczhq-name;hde-firstname; 16380;siz-city;1986/12/15;rawc-street; 127;ND;NORTH DAKOTA + 473;gpwqf-name;pae-firstname; 12820;rga-city;1958/12/19;djsk-street; 131;WY;WYOMING + 474;yvgmq-name;hzp-firstname; 19020;ioc-city;1966/03/22;zdbt-street; 106;FM;FEDERATED STATES OF MICRONESIA + 475;abjqk-name;pdo-firstname; 13040;vnj-city;1962/09/08;kvwv-street; 145;OR;OREGON + 476;adppx-name;gmz-firstname; 16560;pah-city;1962/03/14;ynqs-street; 107;WY;WYOMING + 477;lxcrs-name;arg-firstname; 12160;med-city;1990/03/14;wlag-street; 141;MT;MONTANA + 478;mrkfp-name;jbm-firstname; 19760;dhu-city;1970/06/12;idan-street; 145;UT;UTAH + 479;zmkad-name;vns-firstname; 10080;aoe-city;1955/01/25;qgbd-street; 174;FL;FLORIDA + 480;vftgh-name;nxs-firstname; 11580;igp-city;1954/05/17;fief-street; 183;MT;MONTANA + 481;bnafy-name;geg-firstname; 11160;sfp-city;1974/04/26;tpnq-street; 194;DE;DELAWARE + 482;mwqpn-name;lbw-firstname; 17660;oot-city;1974/04/09;qxgk-street; 18;AK;ALASKA + 483;cijcf-name;uvd-firstname; 17940;ioy-city;1982/02/07;gfiz-street; 147;MN;MINNESOTA + 484;kwjhv-name;swd-firstname; 12400;pue-city;1970/12/10;sall-street; 104;MN;MINNESOTA + 485;mfdfy-name;hsy-firstname; 18260;bzl-city;1982/09/08;hsyc-street; 76;RI;RHODE ISLAND + 486;cdrmm-name;mxa-firstname; 11520;rie-city;1962/12/21;dxed-street; 112;LA;LOUISIANA + 487;flyur-name;mzm-firstname; 12260;wyi-city;1978/07/07;xqoj-street; 156;WV;WEST VIRGINIA + 488;hkwnf-name;obg-firstname; 14520;bib-city;1967/01/20;mvee-street; 115;undefined;undefined + 489;kawax-name;pyn-firstname; 10520;zjh-city;1966/09/01;fsmz-street; 87;DE;DELAWARE + 490;nnhzs-name;sfo-firstname; 19040;thn-city;1990/11/08;tzsb-street; 43;IN;INDIANA + 491;xivec-name;gzo-firstname; 16820;aha-city;1978/10/19;iolt-street; 36;HI;HAWAII + 492;hyiju-name;plw-firstname; 18620;zzu-city;1982/07/13;tydq-street; 112;NV;NEVADA + 493;leylb-name;fcv-firstname; 15720;biw-city;1958/09/18;bnpf-street; 52;MT;MONTANA + 494;pweci-name;hcu-firstname; 19020;fzb-city;1950/10/28;spdv-street; 131;WY;WYOMING + 495;ddulq-name;crh-firstname; 11540;yrm-city;1974/07/31;opds-street; 95;OH;OHIO + 496;fyrha-name;wea-firstname; 16620;vfe-city;1990/12/07;ukki-street; 48;GU;GUAM + 497;vuypf-name;ugz-firstname; 13320;ixw-city;1970/07/09;aptu-street; 60;HI;HAWAII + 498;wezwd-name;oae-firstname; 11180;egb-city;1982/02/27;ldea-street; 2;IL;ILLINOIS + 499;wrokv-name;zaa-firstname; 13980;hac-city;1974/01/20;zwst-street; 21;MO;MISSOURI + 500;wtapc-name;ciu-firstname; 15360;uvh-city;1962/08/03;mddz-street; 196;IA;IOWA + 501;hdtsu-name;two-firstname; 19500;ick-city;1958/11/07;kipe-street; 42;DE;DELAWARE + 502;lwprl-name;jaw-firstname; 14140;acy-city;1970/12/06;jhae-street; 129;MT;MONTANA + 503;iwftp-name;jnp-firstname; 18800;axg-city;1978/10/13;rejy-street; 174;AZ;ARIZONA + 504;qypws-name;fox-firstname; 19820;bzu-city;1966/01/02;owsq-street; 70;AR;ARKANSAS + 505;pzjcf-name;ese-firstname; 12180;kjq-city;1958/11/21;xbyg-street; 12;AS;AMERICAN SAMOA + 506;itzxd-name;erv-firstname; 10460;dsk-city;1978/06/20;baci-street; 151;OH;OHIO + 507;jnpdw-name;zna-firstname; 11000;aqt-city;1966/05/27;pukm-street; 80;WY;WYOMING + 508;dchwr-name;rxe-firstname; 19220;plm-city;1958/05/18;gkgx-street; 100;NH;NEW HAMPSHIRE + 509;pcszz-name;rym-firstname; 15860;tml-city;1983/01/25;qrdz-street; 7;RI;RHODE ISLAND + 510;fatdr-name;rcs-firstname; 17480;ajx-city;1958/09/18;nlal-street; 26;MD;MARYLAND + 511;oblzo-name;wwl-firstname; 17280;hxs-city;1958/06/15;rnpa-street; 20;AR;ARKANSAS + 512;nmflo-name;ljc-firstname; 19720;biq-city;1962/03/23;ypux-street; 197;OR;OREGON + 513;ajhdh-name;iba-firstname; 16920;yru-city;1982/09/08;zedq-street; 148;MN;MINNESOTA + 514;aiewz-name;gla-firstname; 19340;zvj-city;1986/01/12;blie-street; 116;MA;MASSACHUSETTS + 515;tglnr-name;fob-firstname; 14300;ejm-city;1986/09/22;zazt-street; 152;WV;WEST VIRGINIA + 516;tswnt-name;aal-firstname; 19940;jsw-city;1978/07/02;xnjc-street; 125;OK;OKLAHOMA + 517;smukz-name;zim-firstname; 15260;pul-city;1974/09/26;furv-street; 45;IA;IOWA + 518;aygln-name;qfk-firstname; 16700;bmu-city;1958/03/18;esys-street; 148;NE;NEBRASKA + 519;kcuub-name;ffc-firstname; 10720;xnk-city;1958/02/23;cefn-street; 135;AL;ALABAMA + 520;vsujm-name;yne-firstname; 11280;gdr-city;1966/02/12;hdah-street; 70;MS;MISSISSIPPI + 521;tirxh-name;gpy-firstname; 19360;bai-city;1970/04/19;gznh-street; 33;KY;KENTUCKY + 522;wlqnf-name;nnd-firstname; 16120;kij-city;1954/10/07;gorj-street; 85;ID;IDAHO + 523;rynel-name;iaq-firstname; 13640;chy-city;1954/02/01;wbiu-street; 62;WV;WEST VIRGINIA + 524;ohcvr-name;eod-firstname; 18240;lcc-city;1978/12/24;guca-street; 84;LA;LOUISIANA + 525;gabiw-name;gtj-firstname; 17860;ezv-city;1978/01/18;jsyv-street; 143;ND;NORTH DAKOTA + 526;ddajc-name;cab-firstname; 12920;fgz-city;1958/10/06;lkvp-street; 80;NV;NEVADA + 527;kivtx-name;pbs-firstname; 17980;mso-city;1982/10/22;qden-street; 69;IN;INDIANA + 528;tyial-name;mwb-firstname; 17080;pdf-city;1954/02/25;qyym-street; 48;MN;MINNESOTA + 529;ipbyo-name;lcr-firstname; 12040;ygz-city;1978/03/01;qbqk-street; 117;ND;NORTH DAKOTA + 530;tmkxn-name;jhl-firstname; 13460;xkh-city;1966/06/07;wkzu-street; 26;SD;SOUTH DAKOTA + 531;cnkac-name;jxe-firstname; 13140;vgf-city;1974/08/24;sifo-street; 84;ID;IDAHO + 532;zqnwe-name;ujv-firstname; 19020;vjy-city;1966/02/03;sfzz-street; 171;MN;MINNESOTA + 533;aphod-name;vsz-firstname; 11440;spc-city;1962/07/28;alnl-street; 152;FM;FEDERATED STATES OF MICRONESIA + 534;tjber-name;uhp-firstname; 19320;quo-city;1958/12/19;zgus-street; 173;FM;FEDERATED STATES OF MICRONESIA + 535;itblo-name;lbp-firstname; 14840;rgn-city;1962/11/29;qmtb-street; 1;IN;INDIANA + 536;navpk-name;wcs-firstname; 14400;nvr-city;1982/11/01;qlar-street; 126;AK;ALASKA + 537;nkyot-name;zep-firstname; 17720;yim-city;1954/09/30;wwdl-street; 198;ND;NORTH DAKOTA + 538;qebao-name;edw-firstname; 19220;tlh-city;1982/10/16;celb-street; 180;NJ;NEW JERSEY + 539;bzmvi-name;roq-firstname; 10240;beg-city;1974/07/10;nnom-street; 71;KY;KENTUCKY + 540;qtzxt-name;nau-firstname; 17240;fis-city;1990/05/27;fbex-street; 22;AR;ARKANSAS + 541;jarpd-name;mce-firstname; 17060;rgw-city;1982/10/20;eqbd-street; 180;WI;WISCONSIN + 542;ppaal-name;afq-firstname; 17300;fno-city;1982/10/01;zhby-street; 108;NY;NEW YORK + 543;sejhl-name;qwc-firstname; 18000;sfv-city;1986/04/13;xhlx-street; 52;CO;COLORADO + 544;qfvqk-name;mqn-firstname; 10540;rwu-city;1958/07/30;xwsy-street; 169;AL;ALABAMA + 545;ndowg-name;lnm-firstname; 13820;llz-city;1954/08/11;niif-street; 55;KS;KANSAS + 546;hutga-name;ztk-firstname; 19800;uci-city;1950/10/04;ywtn-street; 84;KS;KANSAS + 547;mudul-name;qcs-firstname; 13520;agk-city;1962/04/20;scto-street; 200;CO;COLORADO + 548;mnket-name;edm-firstname; 10040;jqb-city;1954/11/09;avkk-street; 23;VA;VIRGINIA + 549;mbxbn-name;yuu-firstname; 18880;oho-city;1970/11/24;heug-street; 116;PA;PENNSYLVANIA + 550;letcc-name;pyw-firstname; 10160;lzb-city;1966/06/11;wtul-street; 80;RI;RHODE ISLAND + 551;jqovo-name;eir-firstname; 14120;bvr-city;1974/07/01;wdxz-street; 156;SD;SOUTH DAKOTA + 552;jdkan-name;vxc-firstname; 15600;rqa-city;1962/02/07;vroo-street; 182;CO;COLORADO + 553;crzen-name;vwe-firstname; 18800;msz-city;1962/10/17;xxbp-street; 165;IA;IOWA + 554;nogoy-name;swt-firstname; 11580;npe-city;1978/04/15;uath-street; 55;NV;NEVADA + 555;qtkar-name;cdv-firstname; 17020;opk-city;1950/07/26;bsnt-street; 17;AK;ALASKA + 556;oigrk-name;zmz-firstname; 14200;zhd-city;1966/11/30;yeqk-street; 12;DC;DISTRICT OF COLUMBIA + 557;qspmb-name;htl-firstname; 10940;oxl-city;1986/11/17;bqdp-street; 71;NH;NEW HAMPSHIRE + 558;qktka-name;ces-firstname; 12840;xcq-city;1986/02/13;ddqq-street; 62;NE;NEBRASKA + 559;bokzl-name;vfw-firstname; 14160;eyd-city;1970/02/15;ffrm-street; 26;IL;ILLINOIS + 560;eksje-name;xxw-firstname; 18600;vtn-city;1962/12/09;nskq-street; 42;VA;VIRGINIA + 561;avxuw-name;niq-firstname; 13520;cyx-city;1958/05/29;dxit-street; 167;CA;CALIFORNIA + 562;xwskh-name;mud-firstname; 10900;bcd-city;1954/02/15;lqcd-street; 5;VA;VIRGINIA + 563;jrlgi-name;qbl-firstname; 11780;xkb-city;1970/01/27;wfti-street; 101;CO;COLORADO + 564;vvgef-name;qbi-firstname; 17580;dgu-city;1974/10/21;pkhw-street; 140;DE;DELAWARE + 565;vbdsa-name;mqy-firstname; 19520;iqf-city;1958/06/24;gnhg-street; 23;AL;ALABAMA + 566;mimxv-name;idp-firstname; 19280;acp-city;1986/02/10;wziw-street; 1;LA;LOUISIANA + 567;ihvtb-name;jdf-firstname; 17540;xpg-city;1970/11/23;pjvm-street; 156;SC;SOUTH CAROLINA + 568;wztgo-name;njm-firstname; 15200;btu-city;1978/10/30;ihdb-street; 25;OH;OHIO + 569;unnnh-name;zry-firstname; 12300;upy-city;1982/09/15;nbjp-street; 91;GU;GUAM + 570;qtsep-name;spo-firstname; 15940;ryi-city;1983/01/26;kvcq-street; 94;FM;FEDERATED STATES OF MICRONESIA + 571;rznvs-name;cjh-firstname; 15180;duz-city;1974/05/12;igjl-street; 111;AL;ALABAMA + 572;yrdmb-name;lvl-firstname; 12240;pzi-city;1962/11/10;kegb-street; 54;NY;NEW YORK + 573;jdhso-name;hms-firstname; 12620;ova-city;1950/02/14;rxks-street; 115;MS;MISSISSIPPI + 574;seigu-name;rwp-firstname; 15620;yrt-city;1958/09/30;mloc-street; 9;NJ;NEW JERSEY + 575;wzura-name;fzo-firstname; 12500;vgt-city;1974/06/01;tevl-street; 50;NC;NORTH CAROLINA + 576;urqhu-name;hiq-firstname; 15020;ejs-city;1966/04/08;khev-street; 38;AK;ALASKA + 577;yazce-name;zhy-firstname; 12920;ytz-city;1982/05/07;rfsh-street; 125;NH;NEW HAMPSHIRE + 578;okxei-name;qbi-firstname; 17400;plc-city;1966/04/05;mvzs-street; 73;KS;KANSAS + 579;eemhh-name;nsj-firstname; 11660;buq-city;1986/06/23;mftt-street; 191;NE;NEBRASKA + 580;uyoei-name;wiz-firstname; 19000;lqo-city;1974/06/18;rvat-street; 197;NC;NORTH CAROLINA + 581;ahbez-name;gwg-firstname; 10940;gno-city;1978/03/23;rfwr-street; 105;KS;KANSAS + 582;qnopb-name;xgc-firstname; 18800;ayx-city;1962/05/30;lrnc-street; 193;TN;TENNESSEE + 583;beqpz-name;psc-firstname; 10880;sfe-city;1986/07/04;tobm-street; 121;AK;ALASKA + 584;nnvir-name;tbu-firstname; 12860;cuj-city;1962/04/25;wuxg-street; 11;WA;WASHINGTON + 585;cmqrt-name;huk-firstname; 19580;kae-city;1958/09/29;yukq-street; 146;IA;IOWA + 586;ifivh-name;bub-firstname; 15640;pti-city;1950/04/21;alaw-street; 104;VT;VERMONT + 587;nqbqs-name;ndv-firstname; 17500;sxr-city;1963/01/05;zclw-street; 77;MP;NORTHERN MARIANA ISLANDS + 588;kgrlf-name;suz-firstname; 16080;ayf-city;1990/03/07;vmto-street; 120;CO;COLORADO + 589;nhjsx-name;xuh-firstname; 12360;tqo-city;1970/08/04;vfzt-street; 181;undefined;undefined + 590;vdint-name;zal-firstname; 14600;fjh-city;1982/04/22;uopg-street; 117;WV;WEST VIRGINIA + 591;nchqr-name;ldr-firstname; 14700;pyy-city;1966/03/02;nlzm-street; 137;VT;VERMONT + 592;sjmbq-name;tsc-firstname; 19960;dlp-city;1974/05/26;qphd-street; 155;IN;INDIANA + 593;njbsk-name;oft-firstname; 17280;xtc-city;1958/02/13;sebl-street; 85;HI;HAWAII + 594;vtijb-name;obv-firstname; 14000;bpn-city;1962/08/08;mxrl-street; 199;IL;ILLINOIS + 595;ihqjn-name;hnu-firstname; 18960;ztv-city;1982/02/20;mhrl-street; 45;MA;MASSACHUSETTS + 596;kpzko-name;zfz-firstname; 18720;rjc-city;1971/01/01;ycrf-street; 173;CT;CONNECTICUT + 597;qlfgm-name;fnv-firstname; 11360;avw-city;1970/12/01;yvgg-street; 49;LA;LOUISIANA + 598;oeepm-name;asq-firstname; 18660;mvq-city;1990/07/29;ssey-street; 57;OH;OHIO + 599;waklo-name;ksz-firstname; 10660;ddf-city;1958/07/10;dilr-street; 145;NY;NEW YORK + 600;mdftt-name;ldq-firstname; 11460;mkm-city;1966/09/03;rjkt-street; 9;CA;CALIFORNIA + 601;iowsr-name;vmg-firstname; 18420;kie-city;1954/12/30;qfom-street; 117;NE;NEBRASKA + 602;sdtxj-name;zbp-firstname; 16160;gic-city;1962/09/09;eavm-street; 108;IA;IOWA + 603;ocebd-name;img-firstname; 12180;rxp-city;1966/08/18;ygmy-street; 52;PA;PENNSYLVANIA + 604;uvovz-name;nsd-firstname; 13600;tlc-city;1950/08/13;xbsm-street; 110;MP;NORTHERN MARIANA ISLANDS + 605;fzuuf-name;pub-firstname; 10500;ikg-city;1974/04/26;pmnl-street; 189;MA;MASSACHUSETTS + 606;xzvbs-name;qpb-firstname; 17060;xed-city;1950/04/18;vvdx-street; 118;OR;OREGON + 607;qvkir-name;hns-firstname; 12840;kxi-city;1978/03/02;kzbt-street; 65;RI;RHODE ISLAND + 608;llarb-name;psp-firstname; 11240;wlq-city;1990/10/10;qshw-street; 106;AL;ALABAMA + 609;wrzts-name;ygm-firstname; 15520;ybe-city;1974/12/03;uxct-street; 200;ND;NORTH DAKOTA + 610;qfpdp-name;luy-firstname; 16260;qma-city;1978/07/12;opsm-street; 194;RI;RHODE ISLAND + 611;xaosj-name;kfd-firstname; 18100;xmf-city;1982/12/22;hceh-street; 75;LA;LOUISIANA + 612;iuocr-name;ztn-firstname; 19000;sby-city;1970/06/07;qres-street; 86;TX;TEXAS + 613;ocvrm-name;ylm-firstname; 16360;vif-city;1986/11/17;zgov-street; 17;CT;CONNECTICUT + 614;tealr-name;lbu-firstname; 10020;blg-city;1958/03/23;qjvy-street; 182;MS;MISSISSIPPI + 615;ksjja-name;dky-firstname; 15360;qim-city;1986/08/30;rvtm-street; 182;KY;KENTUCKY + 616;jbhpa-name;shc-firstname; 18020;gmc-city;1974/05/29;cvpl-street; 154;AS;AMERICAN SAMOA + 617;vraon-name;nam-firstname; 16320;joa-city;1986/11/07;pnik-street; 194;NM;NEW MEXICO + 618;gtymv-name;lgo-firstname; 16740;ynr-city;1970/05/14;xxzj-street; 74;NJ;NEW JERSEY + 619;sxbpo-name;hyx-firstname; 12900;lvu-city;1958/07/14;tysz-street; 68;HI;HAWAII + 620;yaogj-name;fjg-firstname; 11040;slr-city;1974/06/22;kjoi-street; 134;OK;OKLAHOMA + 621;rprrj-name;vca-firstname; 19160;txr-city;1959/01/18;stjh-street; 137;MA;MASSACHUSETTS + 622;dbaaq-name;jdi-firstname; 13800;uge-city;1954/11/08;ftck-street; 175;OK;OKLAHOMA + 623;eapsq-name;zeu-firstname; 14720;mmh-city;1954/11/01;tjlj-street; 84;AS;AMERICAN SAMOA + 624;wlrqb-name;srf-firstname; 15280;bbq-city;1970/04/06;thvb-street; 17;CO;COLORADO + 625;dzzia-name;ukn-firstname; 14680;zug-city;1954/04/29;nthu-street; 109;LA;LOUISIANA + 626;nfzxa-name;ikc-firstname; 17080;sqt-city;1982/01/21;dteu-street; 113;CT;CONNECTICUT + 627;ovxiz-name;lho-firstname; 10620;gox-city;1954/07/05;cofd-street; 155;NY;NEW YORK + 628;msubu-name;ccj-firstname; 17780;jkv-city;1982/02/25;gfve-street; 43;NV;NEVADA + 629;xxbdr-name;kas-firstname; 17320;ick-city;1970/02/25;gvoi-street; 24;ID;IDAHO + 630;rgryb-name;guv-firstname; 11160;xus-city;1966/05/06;plka-street; 164;FL;FLORIDA + 631;dxprz-name;byy-firstname; 16520;lpb-city;1974/03/01;auoq-street; 103;MH;MARSHALL ISLANDS + 632;nerdb-name;eqa-firstname; 12860;arg-city;1958/03/17;emcz-street; 104;DC;DISTRICT OF COLUMBIA + 633;nszhh-name;nrn-firstname; 16860;hap-city;1974/04/30;zjuq-street; 78;IA;IOWA + 634;jtcxq-name;hxi-firstname; 12160;oqq-city;1982/06/03;xpsp-street; 30;VI;VIRGIN ISLANDS + 635;prglc-name;kqd-firstname; 16080;gqr-city;1978/10/14;pjin-street; 145;NH;NEW HAMPSHIRE + 636;yofsk-name;qch-firstname; 14240;wbz-city;1990/05/28;edsh-street; 5;MI;MICHIGAN + 637;cupcm-name;jvy-firstname; 13720;lkw-city;1966/10/14;frzx-street; 83;NY;NEW YORK + 638;crjjf-name;yfe-firstname; 19880;oof-city;1970/02/07;akkj-street; 98;NC;NORTH CAROLINA + 639;rqyyg-name;hbx-firstname; 12080;sxh-city;1958/04/24;wzjb-street; 159;TX;TEXAS + 640;xxugu-name;yqf-firstname; 10380;ocx-city;1954/11/18;mbps-street; 20;ND;NORTH DAKOTA + 641;ayvhk-name;znx-firstname; 16960;qbw-city;1954/07/23;wgsh-street; 148;OK;OKLAHOMA + 642;xwdxq-name;aik-firstname; 14280;djc-city;1958/06/18;mjpa-street; 130;OH;OHIO + 643;qsvmf-name;lgn-firstname; 17140;mpl-city;1982/06/15;ngot-street; 193;NJ;NEW JERSEY + 644;tlued-name;mcz-firstname; 16780;oab-city;1978/09/22;ppzd-street; 111;AR;ARKANSAS + 645;ibhei-name;djd-firstname; 11340;eep-city;1975/01/24;rzdt-street; 94;MH;MARSHALL ISLANDS + 646;snfrd-name;jof-firstname; 12700;lck-city;1978/12/16;jpcu-street; 184;IN;INDIANA + 647;vkwed-name;elj-firstname; 18940;tdi-city;1990/08/05;ekyx-street; 137;KY;KENTUCKY + 648;acing-name;rxv-firstname; 10660;gtt-city;1970/11/17;ltxb-street; 117;VA;VIRGINIA + 649;fzmkk-name;ilb-firstname; 18540;rwj-city;1986/02/08;xsnm-street; 113;MA;MASSACHUSETTS + 650;knzxd-name;seg-firstname; 17180;oqu-city;1982/11/02;ukda-street; 80;MT;MONTANA + 651;zzyix-name;dlk-firstname; 14180;yoc-city;1986/10/06;jnpv-street; 71;FM;FEDERATED STATES OF MICRONESIA + 652;nsote-name;ivj-firstname; 19740;aqh-city;1990/11/05;faox-street; 77;WI;WISCONSIN + 653;znees-name;edg-firstname; 18120;qkr-city;1986/12/08;prpo-street; 42;DE;DELAWARE + 654;svzbm-name;hsg-firstname; 19180;shh-city;1954/02/05;uglh-street; 150;IA;IOWA + 655;llfur-name;geu-firstname; 12340;qdf-city;1970/12/01;nntt-street; 92;AZ;ARIZONA + 656;pusrl-name;ovn-firstname; 17020;oaa-city;1954/07/30;oxfp-street; 104;AL;ALABAMA + 657;wihqb-name;bkq-firstname; 13540;jau-city;1974/09/25;vuga-street; 83;NY;NEW YORK + 658;ezgdt-name;yaa-firstname; 13380;fdc-city;1986/11/04;yxut-street; 75;VI;VIRGIN ISLANDS + 659;kvtlv-name;zdr-firstname; 15700;bqd-city;1990/06/04;kajd-street; 92;DE;DELAWARE + 660;bqkzl-name;tym-firstname; 14900;rhk-city;1962/02/13;yyzk-street; 165;MH;MARSHALL ISLANDS + 661;ndfve-name;ajt-firstname; 19460;tdr-city;1970/02/04;vpub-street; 143;CT;CONNECTICUT + 662;temdh-name;trc-firstname; 11720;ald-city;1950/05/06;tapb-street; 1;FL;FLORIDA + 663;bzagi-name;dwa-firstname; 13580;zvq-city;1970/05/05;khyi-street; 105;CT;CONNECTICUT + 664;frtvn-name;ukn-firstname; 19400;eto-city;1986/06/29;dcnz-street; 114;MH;MARSHALL ISLANDS + 665;mjdqu-name;xlw-firstname; 14420;lpe-city;1962/08/02;oecb-street; 189;IN;INDIANA + 666;fnopj-name;sdh-firstname; 19080;hxm-city;1974/11/24;pfxz-street; 66;ME;MAINE + 667;kuxaz-name;hye-firstname; 14860;eia-city;1962/09/16;nhdg-street; 198;CO;COLORADO + 668;tfrpe-name;mme-firstname; 12860;mox-city;1978/03/24;uhqo-street; 10;NE;NEBRASKA + 669;eodve-name;tzj-firstname; 11440;syb-city;1986/07/29;jlnw-street; 74;WI;WISCONSIN + 670;slzsi-name;nvw-firstname; 13680;kgz-city;1958/07/13;pptx-street; 88;NY;NEW YORK + 671;wrttz-name;wwx-firstname; 13440;lig-city;1986/11/05;vdph-street; 66;MI;MICHIGAN + 672;wfxhy-name;wtc-firstname; 12760;gnf-city;1970/08/22;itut-street; 3;HI;HAWAII + 673;toeja-name;vrw-firstname; 18900;xes-city;1982/09/01;wpqu-street; 184;undefined;undefined + 674;glmnt-name;bck-firstname; 11680;cuu-city;1978/08/02;wtxv-street; 16;RI;RHODE ISLAND + 675;vyvnk-name;shl-firstname; 10260;xhq-city;1970/06/06;ddbz-street; 28;MA;MASSACHUSETTS + 676;pjdvs-name;adv-firstname; 15600;snf-city;1986/12/10;tdft-street; 133;DC;DISTRICT OF COLUMBIA + 677;rulrt-name;ytn-firstname; 19340;spb-city;1974/11/11;bpwx-street; 24;VA;VIRGINIA + 678;gvttx-name;moj-firstname; 17000;gri-city;1978/11/24;kqlt-street; 27;ID;IDAHO + 679;jtlah-name;gxq-firstname; 15440;jte-city;1978/02/01;mbvv-street; 172;OH;OHIO + 680;cpvhs-name;ruo-firstname; 16080;vhq-city;1970/10/18;ditw-street; 164;AR;ARKANSAS + 681;jikyx-name;hzl-firstname; 13320;hgh-city;1958/08/07;gmqp-street; 85;FL;FLORIDA + 682;bsvsn-name;idb-firstname; 15780;ooc-city;1958/04/28;apff-street; 78;MO;MISSOURI + 683;qgivn-name;bau-firstname; 11580;uez-city;1987/01/29;vpsa-street; 48;RI;RHODE ISLAND + 684;hqbfl-name;fgr-firstname; 11620;ejc-city;1982/02/07;fwsq-street; 107;WA;WASHINGTON + 685;crmtk-name;fmd-firstname; 14480;mdp-city;1962/05/23;knxw-street; 18;IL;ILLINOIS + 686;kccgj-name;pzd-firstname; 10760;ifv-city;1990/05/14;dgfl-street; 172;PR;PUERTO RICO + 687;mddbw-name;lcu-firstname; 11360;crz-city;1970/08/24;eiwf-street; 32;PR;PUERTO RICO + 688;dlnxj-name;tzx-firstname; 11380;nza-city;1966/09/25;jlna-street; 186;MO;MISSOURI + 689;wpjcx-name;hwx-firstname; 13100;slr-city;1987/01/16;wxcr-street; 42;AK;ALASKA + 690;yefnh-name;jzp-firstname; 13080;ozf-city;1986/08/09;zzos-street; 120;OR;OREGON + 691;qsoei-name;kio-firstname; 11860;esa-city;1974/08/04;bhbl-street; 86;HI;HAWAII + 692;xwodh-name;aid-firstname; 17940;wgd-city;1954/07/21;rcrf-street; 90;IL;ILLINOIS + 693;oncpb-name;elk-firstname; 12020;rbp-city;1974/10/28;iblz-street; 114;DE;DELAWARE + 694;qisar-name;kfc-firstname; 19800;alw-city;1958/08/07;ukbj-street; 53;NH;NEW HAMPSHIRE + 695;bvsga-name;qln-firstname; 18060;zlt-city;1966/01/29;rngf-street; 46;WA;WASHINGTON + 696;xguku-name;uxf-firstname; 10080;ezg-city;1974/12/30;icvq-street; 48;UT;UTAH + 697;hxylt-name;hng-firstname; 15360;jgc-city;1966/03/11;lhxd-street; 87;DE;DELAWARE + 698;dmlyv-name;qai-firstname; 20000;hoe-city;1966/08/20;uboj-street; 72;KY;KENTUCKY + 699;kkbjx-name;cnx-firstname; 16780;ndu-city;1970/08/16;afzw-street; 55;NJ;NEW JERSEY + 700;emoio-name;awr-firstname; 18560;qpj-city;1974/11/17;hasx-street; 92;VT;VERMONT + 701;lxpni-name;oox-firstname; 14100;xrj-city;1986/06/09;wvvi-street; 106;MO;MISSOURI + 702;oshuu-name;mim-firstname; 14280;mns-city;1982/09/12;ashj-street; 95;FM;FEDERATED STATES OF MICRONESIA + 703;uhpxg-name;gnt-firstname; 19180;zsi-city;1978/05/22;namc-street; 126;AL;ALABAMA + 704;lvoek-name;reg-firstname; 18920;ubj-city;1954/12/24;nrjw-street; 38;PA;PENNSYLVANIA + 705;lpnhx-name;szp-firstname; 10720;ybd-city;1970/11/26;ntyc-street; 109;GA;GEORGIA + 706;kppvg-name;ztz-firstname; 14660;ocp-city;1990/08/19;ekcr-street; 56;SD;SOUTH DAKOTA + 707;vxwxv-name;qzo-firstname; 15540;rfl-city;1962/03/29;hcwy-street; 116;KY;KENTUCKY + 708;nbgan-name;ocf-firstname; 15240;aas-city;1954/09/14;zkkh-street; 117;IN;INDIANA + 709;faiaw-name;lup-firstname; 16980;rsz-city;1986/09/17;yddi-street; 98;IN;INDIANA + 710;mdmax-name;ggz-firstname; 17360;stn-city;1966/11/25;lwhf-street; 198;IN;INDIANA + 711;etauo-name;pta-firstname; 13360;fnh-city;1978/02/03;gsrn-street; 113;IA;IOWA + 712;gckpz-name;rou-firstname; 17920;liu-city;1974/10/10;apjm-street; 40;AZ;ARIZONA + 713;szvhz-name;ani-firstname; 17240;nab-city;1958/12/10;cxho-street; 87;KY;KENTUCKY + 714;dawkl-name;epe-firstname; 11780;pjg-city;1990/04/24;hdbx-street; 104;WI;WISCONSIN + 715;eifes-name;zka-firstname; 11500;uux-city;1986/10/20;pazo-street; 125;TN;TENNESSEE + 716;fphzw-name;gof-firstname; 14680;dkz-city;1986/06/23;aejx-street; 88;VA;VIRGINIA + 717;bgicm-name;one-firstname; 14820;lwh-city;1966/01/08;dizm-street; 53;AR;ARKANSAS + 718;qnozr-name;fwa-firstname; 17800;rsm-city;1954/04/11;tuun-street; 63;NE;NEBRASKA + 719;zxcjb-name;mhs-firstname; 18620;czu-city;1982/11/15;ucfg-street; 103;PW;PALAU + 720;qqftw-name;dox-firstname; 11280;eii-city;1982/08/03;akjj-street; 174;VT;VERMONT + 721;byevj-name;bah-firstname; 14240;rge-city;1958/01/21;rkkm-street; 142;MD;MARYLAND + 722;ihvvb-name;gaq-firstname; 19140;gua-city;1958/09/30;hxim-street; 88;RI;RHODE ISLAND + 723;suiee-name;vji-firstname; 12120;zuz-city;1970/02/22;ijcn-street; 84;PW;PALAU + 724;fgkoi-name;xkx-firstname; 15880;zuk-city;1970/12/27;frfg-street; 74;IL;ILLINOIS + 725;aowhf-name;hvv-firstname; 12740;mpj-city;1987/01/06;mrmx-street; 95;KY;KENTUCKY + 726;vxkbl-name;xfk-firstname; 18600;odo-city;1970/02/28;jrzc-street; 128;AZ;ARIZONA + 727;zpedp-name;oxh-firstname; 16380;cjz-city;1958/05/28;bnxj-street; 18;GU;GUAM + 728;uqfju-name;dmq-firstname; 15560;ehf-city;1954/03/02;ptqn-street; 68;VA;VIRGINIA + 729;wsoxm-name;xtd-firstname; 15660;ogk-city;1951/01/29;inkh-street; 150;WV;WEST VIRGINIA + 730;cvypk-name;xoy-firstname; 15080;cwr-city;1970/02/28;kyio-street; 117;WV;WEST VIRGINIA + 731;yprgp-name;vgc-firstname; 14660;alm-city;1971/01/05;uoxp-street; 110;CA;CALIFORNIA + 732;udhil-name;dsa-firstname; 12060;lzv-city;1974/02/18;gvvc-street; 149;AK;ALASKA + 733;fgtjr-name;bnt-firstname; 18240;pgf-city;1986/10/15;synp-street; 159;CT;CONNECTICUT + 734;hypgs-name;ycz-firstname; 10840;ufx-city;1966/06/26;frur-street; 81;HI;HAWAII + 735;ijqas-name;bgl-firstname; 18680;dss-city;1950/02/20;rafi-street; 134;ID;IDAHO + 736;seeae-name;pvg-firstname; 10800;txd-city;1958/10/18;uibx-street; 102;GU;GUAM + 737;punzk-name;ubx-firstname; 15020;qzq-city;1978/07/13;cqww-street; 52;PA;PENNSYLVANIA + 738;xyvjy-name;jta-firstname; 19700;koe-city;1986/06/22;lrjw-street; 17;undefined;undefined + 739;ysnek-name;kop-firstname; 13120;bel-city;1950/08/01;xcbk-street; 94;MA;MASSACHUSETTS + 740;ozxng-name;sje-firstname; 15180;nqf-city;1986/11/19;uivr-street; 117;MP;NORTHERN MARIANA ISLANDS + 741;ekqcg-name;hxq-firstname; 16880;jic-city;1966/01/11;grbw-street; 86;UT;UTAH + 742;xxhts-name;krw-firstname; 14140;lwz-city;1978/04/06;fzbe-street; 40;WI;WISCONSIN + 743;uffha-name;jdu-firstname; 13080;jmd-city;1986/06/21;sqbk-street; 66;KS;KANSAS + 744;aevbo-name;bba-firstname; 16880;hgh-city;1958/03/08;urgb-street; 74;NE;NEBRASKA + 745;rqwnb-name;zpb-firstname; 19300;cbx-city;1978/08/12;qavq-street; 5;IN;INDIANA + 746;txozw-name;vet-firstname; 15540;ajt-city;1954/07/05;jayh-street; 105;RI;RHODE ISLAND + 747;xmyzv-name;phl-firstname; 15220;jnk-city;1974/04/04;ckwm-street; 174;KY;KENTUCKY + 748;fusnk-name;iva-firstname; 14660;kqd-city;1982/07/03;zmbq-street; 10;FL;FLORIDA + 749;ksnje-name;eaq-firstname; 12500;gej-city;1982/10/03;vbwg-street; 83;HI;HAWAII + 750;kcahb-name;srs-firstname; 11300;bvz-city;1978/05/16;gyfr-street; 164;LA;LOUISIANA + 751;bzjht-name;vfn-firstname; 17500;mgi-city;1958/06/15;mmko-street; 172;DE;DELAWARE + 752;loydq-name;gft-firstname; 11900;fsr-city;1990/01/20;sjds-street; 62;TN;TENNESSEE + 753;kglxv-name;zlt-firstname; 14960;qgb-city;1982/02/07;gniz-street; 178;AS;AMERICAN SAMOA + 754;loimm-name;ipg-firstname; 15980;sxt-city;1978/10/31;kdlk-street; 22;NY;NEW YORK + 755;demtf-name;yhq-firstname; 12300;pdo-city;1982/09/18;mfye-street; 99;TN;TENNESSEE + 756;jdmdi-name;llu-firstname; 18060;pss-city;1974/07/01;utrs-street; 198;CA;CALIFORNIA + 757;crjfv-name;ccn-firstname; 19520;uub-city;1978/07/12;xtvk-street; 138;OK;OKLAHOMA + 758;irghm-name;dbi-firstname; 15200;otd-city;1982/12/14;iple-street; 144;WY;WYOMING + 759;mcwlu-name;kxy-firstname; 17640;edu-city;1958/07/05;ibtg-street; 173;IN;INDIANA + 760;plfvg-name;cxa-firstname; 19360;rnc-city;1958/04/06;zemc-street; 64;DE;DELAWARE + 761;dnxai-name;gnd-firstname; 11580;ers-city;1966/07/14;ytjt-street; 163;OH;OHIO + 762;ksbwq-name;tku-firstname; 10040;axu-city;1974/08/10;kvel-street; 95;RI;RHODE ISLAND + 763;durgp-name;unr-firstname; 12820;juk-city;1959/01/05;ucky-street; 132;HI;HAWAII + 764;eocqj-name;qcg-firstname; 18940;faf-city;1986/10/14;gjjy-street; 126;OK;OKLAHOMA + 765;dhcim-name;eti-firstname; 17140;aed-city;1982/07/19;syxk-street; 156;FL;FLORIDA + 766;nerhu-name;jfx-firstname; 10140;huk-city;1978/02/07;cyfs-street; 47;ME;MAINE + 767;sxukr-name;hag-firstname; 14400;lfc-city;1963/01/05;wylb-street; 38;WV;WEST VIRGINIA + 768;xznte-name;snc-firstname; 17840;mhl-city;1962/03/23;rfig-street; 151;DE;DELAWARE + 769;tgwig-name;rfn-firstname; 14020;fcs-city;1986/11/12;agpo-street; 79;undefined;undefined + 770;kmkbd-name;fzv-firstname; 12920;kjt-city;1974/03/03;sgjo-street; 63;MN;MINNESOTA + 771;kqzne-name;gvz-firstname; 15500;zar-city;1966/04/01;kvlk-street; 35;DC;DISTRICT OF COLUMBIA + 772;hatuj-name;fwt-firstname; 16040;gih-city;1978/07/10;wbjj-street; 70;MD;MARYLAND + 773;anvjr-name;qqu-firstname; 15280;php-city;1970/09/28;hhnz-street; 147;GU;GUAM + 774;jxiox-name;ein-firstname; 18560;vou-city;1974/04/29;dlcm-street; 52;VA;VIRGINIA + 775;ahdci-name;fhq-firstname; 10040;dis-city;1974/05/17;flyu-street; 84;MO;MISSOURI + 776;slvkr-name;qtq-firstname; 10800;seu-city;1978/08/24;hivm-street; 148;PR;PUERTO RICO + 777;xncyt-name;alj-firstname; 12500;qcq-city;1962/05/20;bqjo-street; 78;VI;VIRGIN ISLANDS + 778;oagkd-name;oud-firstname; 10720;nai-city;1974/09/12;jbps-street; 50;MT;MONTANA + 779;bhinx-name;sde-firstname; 18240;zrt-city;1962/07/08;dxuq-street; 97;PA;PENNSYLVANIA + 780;wpyvx-name;qwg-firstname; 18520;lku-city;1966/03/09;ymkt-street; 133;MD;MARYLAND + 781;wvkan-name;ndd-firstname; 10340;hwn-city;1978/11/18;pqqf-street; 114;NV;NEVADA + 782;biapu-name;kiq-firstname; 16360;lbi-city;1954/10/27;pawe-street; 198;OK;OKLAHOMA + 783;ohygc-name;vxw-firstname; 11100;flm-city;1966/11/20;homq-street; 170;HI;HAWAII + 784;ntbgw-name;msg-firstname; 13740;cth-city;1958/09/28;epdz-street; 80;AS;AMERICAN SAMOA + 785;vegli-name;avd-firstname; 18000;gob-city;1970/08/27;bvnt-street; 32;WY;WYOMING + 786;aadoy-name;gre-firstname; 16040;qln-city;1962/01/03;lqxs-street; 179;ME;MAINE + 787;rzwid-name;xjl-firstname; 16300;eds-city;1970/05/08;hutz-street; 27;TX;TEXAS + 788;qperl-name;nsk-firstname; 13920;hxo-city;1966/11/16;xvzr-street; 160;IA;IOWA + 789;hkhzc-name;hxd-firstname; 19980;gpm-city;1954/08/23;qmrm-street; 60;VA;VIRGINIA + 790;qlixr-name;tsd-firstname; 16780;dvv-city;1978/02/14;ibwa-street; 85;AZ;ARIZONA + 791;ieyhf-name;oox-firstname; 19820;mfp-city;1958/02/17;ntdb-street; 100;MD;MARYLAND + 792;xxtwm-name;zpp-firstname; 12980;udl-city;1970/03/01;dkzb-street; 157;DC;DISTRICT OF COLUMBIA + 793;hvtaa-name;wcr-firstname; 14460;zek-city;1970/01/18;bayt-street; 149;PR;PUERTO RICO + 794;xduae-name;iqk-firstname; 18380;lkm-city;1982/11/24;aujb-street; 62;GA;GEORGIA + 795;qkjcz-name;abw-firstname; 15080;fco-city;1986/11/01;tkuc-street; 69;FM;FEDERATED STATES OF MICRONESIA + 796;jplol-name;akr-firstname; 19760;rvj-city;1970/12/23;sqxe-street; 128;SD;SOUTH DAKOTA + 797;iegsb-name;ujo-firstname; 18480;abx-city;1974/02/24;esda-street; 16;RI;RHODE ISLAND + 798;ypqix-name;hbp-firstname; 13240;kdv-city;1986/05/20;iyaq-street; 10;OH;OHIO + 799;awluy-name;ysq-firstname; 13680;kcv-city;1986/12/16;rtbk-street; 144;ID;IDAHO + 800;vtyuq-name;cup-firstname; 13200;gcq-city;1970/11/05;omwn-street; 156;AR;ARKANSAS + 801;hmvzq-name;jgx-firstname; 18660;hbj-city;1982/03/30;hxik-street; 59;MO;MISSOURI + 802;vhatl-name;ywe-firstname; 10040;ytj-city;1982/02/10;lblz-street; 62;CT;CONNECTICUT + 803;pvozw-name;pln-firstname; 13980;tdu-city;1986/06/18;ulqg-street; 8;MT;MONTANA + 804;mwmvk-name;ttp-firstname; 14020;dcw-city;1982/08/26;lqrt-street; 104;CO;COLORADO + 805;pmlsn-name;uxe-firstname; 17680;udv-city;1962/12/27;usnv-street; 155;PR;PUERTO RICO + 806;uuvrf-name;jxu-firstname; 16360;vpo-city;1978/08/04;tpez-street; 25;MS;MISSISSIPPI + 807;pvcck-name;nnx-firstname; 19780;kfm-city;1962/07/05;aybu-street; 151;CA;CALIFORNIA + 808;kdybn-name;qcg-firstname; 11080;wno-city;1978/05/06;omzx-street; 56;GU;GUAM + 809;ylapy-name;mou-firstname; 11400;yky-city;1990/03/12;rcbx-street; 167;WI;WISCONSIN + 810;ygvfd-name;hqp-firstname; 13240;sfw-city;1958/05/16;svut-street; 16;RI;RHODE ISLAND + 811;wgbsf-name;wgj-firstname; 12000;itm-city;1966/07/31;xlwg-street; 122;NV;NEVADA + 812;guhzx-name;hde-firstname; 19260;kqz-city;1962/03/01;escf-street; 84;NY;NEW YORK + 813;quyjf-name;vta-firstname; 14440;yiw-city;1978/01/29;nsss-street; 83;ND;NORTH DAKOTA + 814;fsbhc-name;ink-firstname; 19220;qjo-city;1974/08/30;pwtn-street; 140;WA;WASHINGTON + 815;zhzwu-name;pkl-firstname; 17680;slh-city;1966/06/13;cspv-street; 106;NJ;NEW JERSEY + 816;sqegy-name;ufg-firstname; 19220;hry-city;1954/03/25;anrd-street; 173;PW;PALAU + 817;bkiln-name;sjs-firstname; 15100;fao-city;1958/09/04;inav-street; 170;AR;ARKANSAS + 818;zynej-name;zgh-firstname; 15100;spc-city;1962/01/25;zewf-street; 85;GU;GUAM + 819;xzasj-name;kib-firstname; 18080;rzn-city;1970/10/07;tlsi-street; 190;SD;SOUTH DAKOTA + 820;tmqlc-name;jes-firstname; 11560;vhc-city;1974/11/27;xxkh-street; 36;MO;MISSOURI + 821;gowim-name;rcr-firstname; 11200;kja-city;1974/06/06;zhkg-street; 41;MO;MISSOURI + 822;bulti-name;cse-firstname; 13660;ukw-city;1970/12/14;glrc-street; 59;NV;NEVADA + 823;evxmh-name;gfc-firstname; 13560;cbo-city;1986/03/05;sfkz-street; 72;ID;IDAHO + 824;dbdop-name;swu-firstname; 14760;tlv-city;1982/10/06;vprj-street; 87;AR;ARKANSAS + 825;mgyeq-name;wko-firstname; 15320;lxz-city;1990/03/22;mfwa-street; 11;DE;DELAWARE + 826;bfplt-name;ard-firstname; 10360;sou-city;1990/02/13;jniu-street; 124;NC;NORTH CAROLINA + 827;dvbot-name;auq-firstname; 16200;qkn-city;1978/03/20;qfzt-street; 9;IL;ILLINOIS + 828;tncjm-name;kvn-firstname; 11640;mlf-city;1954/02/08;igos-street; 166;ID;IDAHO + 829;svoxr-name;rbc-firstname; 10400;bny-city;1958/07/13;socq-street; 198;MH;MARSHALL ISLANDS + 830;jxzxn-name;wau-firstname; 14360;orx-city;1958/07/08;fssh-street; 77;NM;NEW MEXICO + 831;dglke-name;dyc-firstname; 13100;lgz-city;1982/12/18;qixe-street; 158;MH;MARSHALL ISLANDS + 832;pewvm-name;asq-firstname; 10840;czn-city;1966/04/02;wnac-street; 84;MD;MARYLAND + 833;tfgsc-name;vxn-firstname; 10100;wup-city;1982/02/05;qlsb-street; 193;AK;ALASKA + 834;hmoxn-name;vti-firstname; 13660;gvq-city;1974/11/25;dahy-street; 123;HI;HAWAII + 835;bexku-name;mvy-firstname; 13080;wrx-city;1966/09/25;ovag-street; 86;GA;GEORGIA + 836;ksffn-name;ubg-firstname; 18600;gsu-city;1958/02/03;vepx-street; 58;NE;NEBRASKA + 837;ngfsu-name;pgq-firstname; 10400;pag-city;1986/06/19;rwqz-street; 123;DE;DELAWARE + 838;mopuh-name;vdg-firstname; 14160;xbe-city;1970/11/22;vbak-street; 103;MA;MASSACHUSETTS + 839;rifdh-name;eky-firstname; 12900;uwr-city;1978/04/02;yysd-street; 81;GU;GUAM + 840;avlwf-name;xpn-firstname; 14940;ebu-city;1974/08/06;vcki-street; 154;PW;PALAU + 841;avokq-name;dzw-firstname; 14680;xnx-city;1954/02/23;cmis-street; 175;NH;NEW HAMPSHIRE + 842;nahim-name;yvv-firstname; 18020;egx-city;1978/05/05;rpeq-street; 86;NJ;NEW JERSEY + 843;kehud-name;gus-firstname; 13200;bwc-city;1962/02/19;aagh-street; 166;OK;OKLAHOMA + 844;tkfjt-name;kch-firstname; 11160;vuw-city;1974/07/20;mkli-street; 119;WY;WYOMING + 845;gcwwn-name;rpb-firstname; 10640;yum-city;1982/05/17;vqgz-street; 157;OR;OREGON + 846;piobc-name;pii-firstname; 17360;vcs-city;1950/06/02;obsu-street; 105;FM;FEDERATED STATES OF MICRONESIA + 847;nfguz-name;rtb-firstname; 14380;zrh-city;1982/03/01;jltk-street; 164;PW;PALAU + 848;kayxc-name;gew-firstname; 15780;miz-city;1959/01/22;bckb-street; 85;ME;MAINE + 849;kwjrg-name;lwy-firstname; 19080;qym-city;1986/08/26;fagw-street; 181;OR;OREGON + 850;joacl-name;nay-firstname; 15120;wdm-city;1982/12/10;goug-street; 6;VT;VERMONT + 851;fmnme-name;ctx-firstname; 13940;qnj-city;1954/04/12;cacp-street; 155;AL;ALABAMA + 852;cqjtz-name;vqp-firstname; 16480;san-city;1966/10/16;oijg-street; 94;NY;NEW YORK + 853;tdndt-name;qya-firstname; 16320;log-city;1962/05/08;pvtk-street; 151;MP;NORTHERN MARIANA ISLANDS + 854;cquwe-name;lzs-firstname; 12940;vnf-city;1986/08/16;tsfg-street; 181;SD;SOUTH DAKOTA + 855;dbfxb-name;glb-firstname; 18420;ecz-city;1978/11/28;fpuk-street; 42;PW;PALAU + 856;odwcc-name;tcz-firstname; 17420;ffa-city;1954/06/12;kirb-street; 158;WI;WISCONSIN + 857;qctkr-name;wpo-firstname; 16980;upo-city;1978/06/21;gfrm-street; 116;TX;TEXAS + 858;qikks-name;eep-firstname; 18560;tof-city;1986/08/11;trqf-street; 64;WA;WASHINGTON + 859;gbltk-name;jny-firstname; 12080;tdu-city;1978/09/15;wsii-street; 133;ID;IDAHO + 860;cirdu-name;dzw-firstname; 11340;stm-city;1974/04/04;qnat-street; 48;PA;PENNSYLVANIA + 861;xukav-name;rep-firstname; 18240;pzu-city;1950/11/16;piru-street; 24;PW;PALAU + 862;dqxqx-name;eca-firstname; 16080;ddl-city;1982/08/15;snln-street; 109;AK;ALASKA + 863;jxcxv-name;ipw-firstname; 19200;ctv-city;1982/12/03;zazz-street; 171;TN;TENNESSEE + 864;ddgxr-name;bwx-firstname; 18860;giq-city;1954/05/11;ukyp-street; 161;SC;SOUTH CAROLINA + 865;wcboj-name;cqb-firstname; 15080;srk-city;1978/04/28;pycv-street; 12;PW;PALAU + 866;ifgzt-name;bde-firstname; 17700;whw-city;1978/05/05;qfgv-street; 162;SD;SOUTH DAKOTA + 867;riwqf-name;qil-firstname; 18480;mce-city;1990/09/07;qxyc-street; 69;MP;NORTHERN MARIANA ISLANDS + 868;zdudw-name;yxg-firstname; 17380;apv-city;1954/09/09;wymd-street; 195;KS;KANSAS + 869;szvfk-name;nei-firstname; 13660;xqq-city;1974/10/14;ygbr-street; 129;KY;KENTUCKY + 870;lrgna-name;lqv-firstname; 16840;woh-city;1978/09/16;ibzb-street; 87;IL;ILLINOIS + 871;jeaqx-name;kdj-firstname; 16840;lpn-city;1979/01/26;guxb-street; 10;WA;WASHINGTON + 872;ubvth-name;njr-firstname; 14920;hbz-city;1966/10/20;knnm-street; 69;AL;ALABAMA + 873;opkuq-name;sbc-firstname; 10520;wpu-city;1974/10/24;tzoa-street; 10;OK;OKLAHOMA + 874;eeesy-name;sei-firstname; 13420;xhc-city;1978/01/10;wsmi-street; 48;AK;ALASKA + 875;njzrw-name;yep-firstname; 13680;hdj-city;1974/05/12;bhba-street; 42;ME;MAINE + 876;caria-name;jaf-firstname; 18660;scx-city;1962/12/18;envv-street; 158;GU;GUAM + 877;sdwui-name;dax-firstname; 17780;mdr-city;1958/12/22;orah-street; 167;AK;ALASKA + 878;pnklv-name;not-firstname; 11160;gfk-city;1982/08/07;muqk-street; 6;AZ;ARIZONA + 879;xhqwk-name;cgz-firstname; 19940;nji-city;1962/05/06;ihqj-street; 97;TN;TENNESSEE + 880;oigoy-name;ccp-firstname; 14060;ase-city;1958/05/20;faef-street; 133;MD;MARYLAND + 881;nysrz-name;nil-firstname; 17700;ile-city;1966/09/13;aqiy-street; 25;WI;WISCONSIN + 882;wrkvm-name;ftf-firstname; 17780;wim-city;1982/12/12;kzed-street; 57;MN;MINNESOTA + 883;zngvz-name;tcp-firstname; 11640;vba-city;1982/05/29;phle-street; 105;OK;OKLAHOMA + 884;wdubl-name;his-firstname; 19840;ybx-city;1978/08/06;upqg-street; 80;TN;TENNESSEE + 885;fxvpc-name;yxz-firstname; 13380;ocv-city;1958/04/08;shtt-street; 28;IL;ILLINOIS + 886;blbvw-name;nln-firstname; 17740;gwm-city;1955/01/28;ratd-street; 98;UT;UTAH + 887;ajxnx-name;utp-firstname; 15540;eak-city;1950/07/01;zlzg-street; 74;OR;OREGON + 888;aeeid-name;bmc-firstname; 19100;qtd-city;1958/10/28;sgwc-street; 127;OH;OHIO + 889;ypwhc-name;ojx-firstname; 11480;zib-city;1967/01/09;gpfp-street; 76;UT;UTAH + 890;kuxdp-name;exg-firstname; 14840;cvh-city;1986/01/22;jbif-street; 108;TN;TENNESSEE + 891;ujwmw-name;aqz-firstname; 14080;mwz-city;1978/07/22;eggm-street; 8;MN;MINNESOTA + 892;xogup-name;pps-firstname; 11740;bic-city;1970/11/26;thtu-street; 34;ME;MAINE + 893;vmrty-name;rmg-firstname; 11060;cna-city;1970/08/27;uknc-street; 24;GA;GEORGIA + 894;rrwco-name;dji-firstname; 18380;oln-city;1974/01/02;fzqy-street; 129;MI;MICHIGAN + 895;dvcbl-name;nam-firstname; 15040;sbp-city;1962/03/02;txma-street; 170;HI;HAWAII + 896;tbsrx-name;krm-firstname; 15220;gao-city;1959/01/03;fmhf-street; 125;TX;TEXAS + 897;ymjve-name;nmc-firstname; 19280;yux-city;1974/12/11;ycqn-street; 65;FL;FLORIDA + 898;rycyz-name;prd-firstname; 15920;oml-city;1982/09/13;ivie-street; 30;OK;OKLAHOMA + 899;agxhw-name;zla-firstname; 14780;pkw-city;1970/04/28;mcrt-street; 139;MN;MINNESOTA + 900;bbdii-name;wkd-firstname; 15220;eup-city;1962/05/01;neqn-street; 191;ID;IDAHO + 901;svszz-name;gyt-firstname; 12900;sav-city;1954/09/30;gqnm-street; 117;SC;SOUTH CAROLINA + 902;dqidv-name;ooi-firstname; 17340;krq-city;1978/06/24;hjtm-street; 106;NJ;NEW JERSEY + 903;blcfn-name;lrz-firstname; 19240;sav-city;1978/03/01;xrfr-street; 8;RI;RHODE ISLAND + 904;acgjp-name;ncp-firstname; 17900;snk-city;1974/10/27;qbrs-street; 65;IA;IOWA + 905;eloeo-name;tmh-firstname; 17500;use-city;1971/01/05;hkrt-street; 23;PA;PENNSYLVANIA + 906;lxogw-name;hlp-firstname; 10960;yhi-city;1990/10/25;ewnk-street; 7;GU;GUAM + 907;gvwtl-name;dmy-firstname; 19920;fqc-city;1970/05/02;mpze-street; 63;LA;LOUISIANA + 908;vrrxx-name;xmg-firstname; 17160;yks-city;1978/08/14;hikr-street; 165;PW;PALAU + 909;hkpuu-name;ofx-firstname; 17040;hid-city;1978/02/03;rkrn-street; 8;UT;UTAH + 910;bupib-name;ftw-firstname; 16620;fsw-city;1971/01/09;uzxm-street; 95;RI;RHODE ISLAND + 911;siwci-name;fir-firstname; 11180;sqp-city;1974/06/27;pgeq-street; 134;AS;AMERICAN SAMOA + 912;mpkrp-name;etl-firstname; 10560;kty-city;1958/06/02;cpug-street; 67;CA;CALIFORNIA + 913;itfnp-name;ncv-firstname; 16440;dgw-city;1958/07/29;xdkl-street; 131;OK;OKLAHOMA + 914;oyvqm-name;lwu-firstname; 14760;dzo-city;1958/07/09;ubqh-street; 57;IA;IOWA + 915;kuila-name;umo-firstname; 11880;rhy-city;1982/09/17;jwha-street; 81;AK;ALASKA + 916;zduzo-name;gmh-firstname; 12360;oxv-city;1990/06/26;dbxf-street; 76;VI;VIRGIN ISLANDS + 917;cxggf-name;fld-firstname; 12900;sif-city;1954/09/08;wdis-street; 142;WI;WISCONSIN + 918;euelo-name;sbi-firstname; 19340;shq-city;1986/04/20;qmja-street; 3;GA;GEORGIA + 919;papgo-name;oyu-firstname; 10280;bet-city;1970/06/19;otzt-street; 155;CA;CALIFORNIA + 920;dtjeu-name;gvg-firstname; 11980;aie-city;1954/10/17;tcpt-street; 77;NY;NEW YORK + 921;xryzl-name;oqb-firstname; 16540;era-city;1962/08/27;aywk-street; 118;VI;VIRGIN ISLANDS + 922;pydqj-name;rwh-firstname; 15640;ekn-city;1978/09/16;yecx-street; 30;WA;WASHINGTON + 923;picbf-name;cru-firstname; 16040;iaa-city;1986/07/01;ttsg-street; 112;MO;MISSOURI + 924;jfqgk-name;ghq-firstname; 10420;yap-city;1958/06/19;qzbd-street; 1;NV;NEVADA + 925;iqoex-name;dha-firstname; 17340;fqq-city;1966/05/09;mbkz-street; 151;VT;VERMONT + 926;xlugu-name;fae-firstname; 12060;uvm-city;1954/11/21;dhin-street; 100;CT;CONNECTICUT + 927;pxbfc-name;cia-firstname; 15160;xrr-city;1986/06/20;psch-street; 122;MT;MONTANA + 928;koqls-name;teq-firstname; 16100;vtl-city;1982/04/28;qfxy-street; 11;ME;MAINE + 929;nrhco-name;cwj-firstname; 17380;zzw-city;1958/06/19;dcir-street; 188;WA;WASHINGTON + 930;rrxbl-name;gfc-firstname; 13160;ruo-city;1982/06/02;yers-street; 108;MH;MARSHALL ISLANDS + 931;rqtri-name;oqi-firstname; 19780;jvk-city;1970/12/08;dfnt-street; 196;IA;IOWA + 932;txjky-name;gvy-firstname; 19040;jwu-city;1982/03/01;pacb-street; 83;UT;UTAH + 933;efrbq-name;tgu-firstname; 13200;xgk-city;1970/04/10;ipjk-street; 156;WI;WISCONSIN + 934;xzrxi-name;lwt-firstname; 16980;zll-city;1982/01/27;geic-street; 42;LA;LOUISIANA + 935;uafzk-name;wox-firstname; 10240;agj-city;1978/07/03;wdwx-street; 13;TX;TEXAS + 936;npomk-name;upa-firstname; 19060;oad-city;1986/06/27;qkua-street; 200;MP;NORTHERN MARIANA ISLANDS + 937;bepdw-name;box-firstname; 13280;sxr-city;1978/04/15;rnub-street; 19;ME;MAINE + 938;qbbcf-name;igs-firstname; 15440;xrs-city;1958/09/13;yhum-street; 176;MA;MASSACHUSETTS + 939;hirau-name;vyi-firstname; 15980;iyv-city;1962/09/17;gqqy-street; 48;FM;FEDERATED STATES OF MICRONESIA + 940;fzpiq-name;rco-firstname; 10660;wpc-city;1978/03/30;kktg-street; 149;NY;NEW YORK + 941;kvtgo-name;wkl-firstname; 13840;yik-city;1962/02/18;goyb-street; 15;NV;NEVADA + 942;mpljt-name;xyx-firstname; 14880;kdx-city;1982/05/25;fkgb-street; 50;WV;WEST VIRGINIA + 943;cxjtz-name;zkx-firstname; 18500;mrg-city;1974/09/04;seup-street; 139;MP;NORTHERN MARIANA ISLANDS + 944;cbkvb-name;uji-firstname; 13260;rtg-city;1954/02/08;oedv-street; 5;AZ;ARIZONA + 945;qsujg-name;hig-firstname; 11980;obk-city;1974/12/24;awap-street; 140;WI;WISCONSIN + 946;faqty-name;wml-firstname; 19180;qmg-city;1958/07/06;zxlq-street; 9;NV;NEVADA + 947;cdwge-name;aeu-firstname; 16940;axh-city;1970/12/16;nkhw-street; 165;RI;RHODE ISLAND + 948;ctmij-name;tqq-firstname; 12600;apv-city;1990/12/10;egyw-street; 94;MD;MARYLAND + 949;axcvk-name;dhn-firstname; 11900;osf-city;1982/03/17;tzwx-street; 71;OH;OHIO + 950;flhxd-name;tty-firstname; 10380;nfn-city;1970/04/16;iweq-street; 144;MN;MINNESOTA + 951;bnnjj-name;ttq-firstname; 11360;shi-city;1966/11/16;xhde-street; 125;MI;MICHIGAN + 952;ohowr-name;rlf-firstname; 16380;dzs-city;1982/05/12;yiwv-street; 46;IL;ILLINOIS + 953;ofqhs-name;pyy-firstname; 12100;pfy-city;1958/04/28;xqtd-street; 148;UT;UTAH + 954;quyhm-name;oln-firstname; 12140;lqe-city;1978/09/28;prlu-street; 151;MO;MISSOURI + 955;ailpr-name;cut-firstname; 13200;yqu-city;1950/07/05;fxsk-street; 101;IA;IOWA + 956;zeyok-name;vhb-firstname; 17240;nih-city;1986/11/30;bvms-street; 86;WA;WASHINGTON + 957;xrfau-name;owh-firstname; 17180;tep-city;1954/03/02;qcmo-street; 45;NJ;NEW JERSEY + 958;nuoiw-name;rxs-firstname; 11320;uef-city;1962/04/23;oiuf-street; 117;MA;MASSACHUSETTS + 959;imjdp-name;mlh-firstname; 19020;mpi-city;1962/06/28;obxd-street; 11;PA;PENNSYLVANIA + 960;ynmgr-name;uzp-firstname; 15500;oxs-city;1986/07/15;epkx-street; 92;GA;GEORGIA + 961;wcucp-name;qlt-firstname; 12460;sow-city;1986/05/17;qoxd-street; 40;WV;WEST VIRGINIA + 962;ugiex-name;ebp-firstname; 15640;vgo-city;1974/09/28;ggdc-street; 22;MH;MARSHALL ISLANDS + 963;nsilq-name;vxf-firstname; 19280;zzm-city;1986/03/13;kxrw-street; 68;VA;VIRGINIA + 964;whtok-name;oqs-firstname; 18340;ddf-city;1954/10/13;toaz-street; 100;VT;VERMONT + 965;lbwzb-name;rgg-firstname; 15340;bla-city;1978/12/10;usrd-street; 175;TN;TENNESSEE + 966;lcbat-name;nnd-firstname; 16200;ebl-city;1990/11/20;emud-street; 144;MI;MICHIGAN + 967;alouv-name;omr-firstname; 14980;ouw-city;1958/06/11;xbnb-street; 90;AZ;ARIZONA + 968;nvqwv-name;blm-firstname; 17880;nip-city;1974/12/14;sgzi-street; 33;AS;AMERICAN SAMOA + 969;wdidq-name;qwd-firstname; 15480;eis-city;1979/01/22;qqbv-street; 43;NH;NEW HAMPSHIRE + 970;jmlfw-name;gal-firstname; 12680;fjt-city;1955/01/10;iqnm-street; 81;GU;GUAM + 971;feoxv-name;mme-firstname; 18020;oed-city;1986/01/13;jtud-street; 73;NV;NEVADA + 972;gsdoz-name;oxc-firstname; 10420;jff-city;1978/02/20;jnom-street; 62;OR;OREGON + 973;hklot-name;rur-firstname; 13900;nur-city;1986/06/05;nkmx-street; 195;SD;SOUTH DAKOTA + 974;smemp-name;ehc-firstname; 10080;wrv-city;1950/04/10;wfrv-street; 93;FL;FLORIDA + 975;htgft-name;nzy-firstname; 16960;gwe-city;1966/05/09;abiz-street; 58;UT;UTAH + 976;jaikt-name;tls-firstname; 17280;whv-city;1966/09/19;wfzd-street; 70;ME;MAINE + 977;yshns-name;zhh-firstname; 13200;dty-city;1978/12/13;hiht-street; 158;KS;KANSAS + 978;selvf-name;zcr-firstname; 12900;phm-city;1958/12/28;pont-street; 149;GU;GUAM + 979;qoxyo-name;oth-firstname; 10280;uic-city;1986/03/16;havi-street; 153;ID;IDAHO + 980;uabxl-name;cid-firstname; 18000;cea-city;1982/02/18;jykd-street; 127;WV;WEST VIRGINIA + 981;hjnpb-name;wwh-firstname; 12400;fwd-city;1978/10/01;ryqy-street; 101;OH;OHIO + 982;zcqum-name;ecp-firstname; 15820;rpu-city;1958/11/11;fpoi-street; 118;undefined;undefined + 983;cetkk-name;btl-firstname; 18660;xrq-city;1970/04/26;mkuo-street; 167;TX;TEXAS + 984;laofd-name;fqf-firstname; 18180;ddy-city;1986/04/01;qjar-street; 14;OR;OREGON + 985;xjtie-name;tpq-firstname; 16800;fgb-city;1970/03/09;nqpk-street; 108;MA;MASSACHUSETTS + 986;txrwc-name;ygc-firstname; 17880;ipb-city;1962/04/26;zomj-street; 189;DC;DISTRICT OF COLUMBIA + 987;csicd-name;yzm-firstname; 18240;nwu-city;1978/05/03;ishz-street; 24;LA;LOUISIANA + 988;njtnl-name;nxa-firstname; 10360;ols-city;1990/12/01;joem-street; 101;DE;DELAWARE + 989;dclnv-name;hve-firstname; 17080;amp-city;1978/05/22;jfuv-street; 17;WI;WISCONSIN + 990;jrazh-name;yec-firstname; 14000;tsi-city;1986/09/02;guzz-street; 119;KS;KANSAS + 991;hpxko-name;psn-firstname; 11900;ocf-city;1970/04/21;gvbf-street; 12;OK;OKLAHOMA + 992;lggcw-name;sey-firstname; 17260;ike-city;1958/09/19;yrnf-street; 34;NE;NEBRASKA + 993;cxifr-name;agu-firstname; 14360;ujj-city;1954/09/28;widd-street; 127;undefined;undefined + 994;kfwgh-name;qaa-firstname; 17900;kis-city;1970/10/02;wwgu-street; 134;MD;MARYLAND + 995;pcizt-name;fdt-firstname; 15320;tzn-city;1982/09/08;nqii-street; 163;TN;TENNESSEE + 996;hyskd-name;ott-firstname; 19800;zja-city;1962/03/05;tjpp-street; 108;OH;OHIO + 997;nfdtl-name;jxu-firstname; 12980;isx-city;1982/07/05;dldc-street; 166;CA;CALIFORNIA + 998;xtukc-name;aih-firstname; 15360;ctn-city;1974/11/10;qhwy-street; 102;MI;MICHIGAN + 999;qfwcj-name;fdu-firstname; 10100;enp-city;1950/01/11;qqga-street; 19;TX;TEXAS + 1000;uvhph-name;vri-firstname; 11920;tjs-city;1974/04/28;czih-street; 151;NE;NEBRASKA diff --git a/integration-tests/spark-native/files/state-population.txt b/integration-tests/spark-native/files/state-population.txt new file mode 100644 index 00000000000..ea6059c7e69 --- /dev/null +++ b/integration-tests/spark-native/files/state-population.txt @@ -0,0 +1,51 @@ +Alabama;4874747 +ALASKA;739795 +Arizona;7016270 +Arkansas;3004279 +CALIFORNIA;39536653 +Colorado;5607154 +Connecticut;3588184 +Delaware;961939 +District of Columbia;693972 +FLORIDA;20984400 +Georgia;10429379 +Hawaii;1427538 +Idaho;1716943 +Illinois;12802023 +INDIANA;6666818 +Iowa;3145711 +Kansas;2913123 +Kentucky;4454189 +Louisiana;4684333 +Maine;1335907 +Maryland;6052177 +Massachusetts;6859819 +Michigan;9962311 +Minnesota;5576606 +Mississippi;2984100 +Missouri;6113532 +Montana;1050493 +NEBRASKA;1920076 +Nevada;2998039 +New Hampshire;1342795 +New Jersey;9005644 +New Mexico;2088070 +NEW YORK;19849399 +North Carolina;10273419 +North Dakota;755393 +Ohio;11658609 +Oklahoma;3930864 +Oregon;4142776 +Pennsylvania;12805537 +Rhode Island;1059639 +South Carolina;5024369 +South Dakota;869666 +Tennessee;6715984 +TEXAS;28304596 +Utah;3101833 +Vermont;623657 +Virginia;8470020 +WASHINGTON;7405743 +West Virginia;1815857 +Wisconsin;5795483 +Wyoming;579315 diff --git a/integration-tests/spark-native/hop-config.json b/integration-tests/spark-native/hop-config.json new file mode 100644 index 00000000000..d9e1e6562e0 --- /dev/null +++ b/integration-tests/spark-native/hop-config.json @@ -0,0 +1,290 @@ +{ + "variables": [ + { + "name": "HOP_LENIENT_STRING_TO_NUMBER_CONVERSION", + "value": "N", + "description": "System wide flag to allow lenient string to number conversion for backward compatibility. If this setting is set to \"Y\", an string starting with digits will be converted successfully into a number. (example: 192.168.1.1 will be converted into 192 or 192.168 or 192168 depending on the decimal and grouping symbol). The default (N) will be to throw an error if non-numeric symbols are found in the string." + }, + { + "name": "HOP_COMPATIBILITY_DB_IGNORE_TIMEZONE", + "value": "N", + "description": "System wide flag to ignore timezone while writing date/timestamp value to the database." + }, + { + "name": "HOP_LOG_SIZE_LIMIT", + "value": "0", + "description": "The log size limit for all pipelines and workflows that don't have the \"log size limit\" property set in their respective properties." + }, + { + "name": "HOP_EMPTY_STRING_DIFFERS_FROM_NULL", + "value": "N", + "description": "NULL vs Empty String. If this setting is set to Y, an empty string and null are different. Otherwise they are not." + }, + { + "name": "HOP_MAX_LOG_SIZE_IN_LINES", + "value": "0", + "description": "The maximum number of log lines that are kept internally by Hop. Set to 0 to keep all rows (default)" + }, + { + "name": "HOP_MAX_LOG_TIMEOUT_IN_MINUTES", + "value": "1440", + "description": "The maximum age (in minutes) of a log line while being kept internally by Hop. Set to 0 to keep all rows indefinitely (default)" + }, + { + "name": "HOP_MAX_WORKFLOW_TRACKER_SIZE", + "value": "5000", + "description": "The maximum number of workflow trackers kept in memory" + }, + { + "name": "HOP_MAX_ACTIONS_LOGGED", + "value": "5000", + "description": "The maximum number of action results kept in memory for logging purposes." + }, + { + "name": "HOP_MAX_LOGGING_REGISTRY_SIZE", + "value": "10000", + "description": "The maximum number of logging registry entries kept in memory for logging purposes." + }, + { + "name": "HOP_LOG_TAB_REFRESH_DELAY", + "value": "1000", + "description": "The hop log tab refresh delay." + }, + { + "name": "HOP_LOG_TAB_REFRESH_PERIOD", + "value": "1000", + "description": "The hop log tab refresh period." + }, + { + "name": "HOP_PLUGIN_CLASSES", + "value": null, + "description": "A comma delimited list of classes to scan for plugin annotations" + }, + { + "name": "HOP_PLUGIN_PACKAGES", + "value": null, + "description": "A comma delimited list of packages to scan for plugin annotations (warning: slow!!)" + }, + { + "name": "HOP_TRANSFORM_PERFORMANCE_SNAPSHOT_LIMIT", + "value": "0", + "description": "The maximum number of transform performance snapshots to keep in memory. Set to 0 to keep all snapshots indefinitely (default)" + }, + { + "name": "HOP_ROWSET_GET_TIMEOUT", + "value": "50", + "description": "The name of the variable that optionally contains an alternative rowset get timeout (in ms). This only makes a difference for extremely short lived pipelines." + }, + { + "name": "HOP_ROWSET_PUT_TIMEOUT", + "value": "50", + "description": "The name of the variable that optionally contains an alternative rowset put timeout (in ms). This only makes a difference for extremely short lived pipelines." + }, + { + "name": "HOP_CORE_TRANSFORMS_FILE", + "value": null, + "description": "The name of the project variable that will contain the alternative location of the hop-transforms.xml file. You can use this to customize the list of available internal transforms outside of the codebase." + }, + { + "name": "HOP_CORE_WORKFLOW_ACTIONS_FILE", + "value": null, + "description": "The name of the project variable that will contain the alternative location of the hop-workflow-actions.xml file." + }, + { + "name": "HOP_SERVER_OBJECT_TIMEOUT_MINUTES", + "value": "1440", + "description": "This project variable will set a time-out after which waiting, completed or stopped pipelines and workflows will be automatically cleaned up. The default value is 1440 (one day)." + }, + { + "name": "HOP_PIPELINE_PAN_JVM_EXIT_CODE", + "value": null, + "description": "Set this variable to an integer that will be returned as the Pan JVM exit code." + }, + { + "name": "HOP_DISABLE_CONSOLE_LOGGING", + "value": "N", + "description": "Set this variable to Y to disable standard Hop logging to the console. (stdout)" + }, + { + "name": "HOP_REDIRECT_STDERR", + "value": "N", + "description": "Set this variable to Y to redirect stderr to Hop logging." + }, + { + "name": "HOP_REDIRECT_STDOUT", + "value": "N", + "description": "Set this variable to Y to redirect stdout to Hop logging." + }, + { + "name": "HOP_DEFAULT_NUMBER_FORMAT", + "value": null, + "description": "The name of the variable containing an alternative default number format" + }, + { + "name": "HOP_DEFAULT_BIGNUMBER_FORMAT", + "value": null, + "description": "The name of the variable containing an alternative default bignumber format" + }, + { + "name": "HOP_DEFAULT_INTEGER_FORMAT", + "value": null, + "description": "The name of the variable containing an alternative default integer format" + }, + { + "name": "HOP_DEFAULT_DATE_FORMAT", + "value": null, + "description": "The name of the variable containing an alternative default date format" + }, + { + "name": "HOP_DEFAULT_TIMESTAMP_FORMAT", + "value": null, + "description": "The name of the variable containing an alternative default timestamp format" + }, + { + "name": "HOP_DEFAULT_SERVLET_ENCODING", + "value": null, + "description": "Defines the default encoding for servlets, leave it empty to use Java default encoding" + }, + { + "name": "HOP_FAIL_ON_LOGGING_ERROR", + "value": "N", + "description": "Set this variable to Y when you want the workflow/pipeline fail with an error when the related logging process (e.g. to a database) fails." + }, + { + "name": "HOP_AGGREGATION_MIN_NULL_IS_VALUED", + "value": "N", + "description": "Set this variable to Y to set the minimum to NULL if NULL is within an aggregate. Otherwise by default NULL is ignored by the MIN aggregate and MIN is set to the minimum value that is not NULL. See also the variable HOP_AGGREGATION_ALL_NULLS_ARE_ZERO." + }, + { + "name": "HOP_AGGREGATION_ALL_NULLS_ARE_ZERO", + "value": "N", + "description": "Set this variable to Y to return 0 when all values within an aggregate are NULL. Otherwise by default a NULL is returned when all values are NULL." + }, + { + "name": "HOP_COMPATIBILITY_TEXT_FILE_OUTPUT_APPEND_NO_HEADER", + "value": "N", + "description": "Set this variable to Y for backward compatibility for the Text File Output transform. Setting this to Ywill add no header row at all when the append option is enabled, regardless if the file is existing or not." + }, + { + "name": "HOP_PASSWORD_ENCODER_PLUGIN", + "value": "Hop", + "description": "Specifies the password encoder plugin to use by ID (Hop is the default)." + }, + { + "name": "HOP_SYSTEM_HOSTNAME", + "value": null, + "description": "You can use this variable to speed up hostname lookup. Hostname lookup is performed by Hop so that it is capable of logging the server on which a workflow or pipeline is executed." + }, + { + "name": "HOP_SERVER_JETTY_ACCEPTORS", + "value": null, + "description": "A variable to configure jetty option: acceptors for Carte" + }, + { + "name": "HOP_SERVER_JETTY_ACCEPT_QUEUE_SIZE", + "value": null, + "description": "A variable to configure jetty option: acceptQueueSize for Carte" + }, + { + "name": "HOP_SERVER_JETTY_RES_MAX_IDLE_TIME", + "value": null, + "description": "A variable to configure jetty option: lowResourcesMaxIdleTime for Carte" + }, + { + "name": "HOP_COMPATIBILITY_MERGE_ROWS_USE_REFERENCE_STREAM_WHEN_IDENTICAL", + "value": "N", + "description": "Set this variable to Y for backward compatibility for the Merge Rows (diff) transform. Setting this to Y will use the data from the reference stream (instead of the comparison stream) in case the compared rows are identical." + }, + { + "name": "HOP_SPLIT_FIELDS_REMOVE_ENCLOSURE", + "value": "false", + "description": "Set this variable to false to preserve enclosure symbol after splitting the string in the Split fields transform. Changing it to true will remove first and last enclosure symbol from the resulting string chunks." + }, + { + "name": "HOP_ALLOW_EMPTY_FIELD_NAMES_AND_TYPES", + "value": "false", + "description": "Set this variable to TRUE to allow your pipeline to pass 'null' fields and/or empty types." + }, + { + "name": "HOP_GLOBAL_LOG_VARIABLES_CLEAR_ON_EXPORT", + "value": "false", + "description": "Set this variable to false to preserve global log variables defined in pipeline / workflow Properties -> Log panel. Changing it to true will clear it when export pipeline / workflow." + }, + { + "name": "HOP_FILE_OUTPUT_MAX_STREAM_COUNT", + "value": "1024", + "description": "This project variable is used by the Text File Output transform. It defines the max number of simultaneously open files within the transform. The transform will close/reopen files as necessary to insure the max is not exceeded" + }, + { + "name": "HOP_FILE_OUTPUT_MAX_STREAM_LIFE", + "value": "0", + "description": "This project variable is used by the Text File Output transform. It defines the max number of milliseconds between flushes of files opened by the transform." + }, + { + "name": "HOP_USE_NATIVE_FILE_DIALOG", + "value": "N", + "description": "Set this value to Y if you want to use the system file open/save dialog when browsing files" + }, + { + "name": "HOP_AUTO_CREATE_CONFIG", + "value": "Y", + "description": "Set this value to N if you don't want to automatically create a hop configuration file (hop-config.json) when it's missing" + } + ], + "LocaleDefault": "en_BE", + "guiProperties": { + "FontFixedSize": "13", + "MaxUndo": "100", + "DarkMode": "Y", + "FontNoteSize": "13", + "ShowOSLook": "Y", + "FontFixedStyle": "0", + "FontNoteName": ".AppleSystemUIFont", + "FontFixedName": "Monospaced", + "FontGraphStyle": "0", + "FontDefaultSize": "13", + "GraphColorR": "255", + "FontGraphSize": "13", + "IconSize": "32", + "BackgroundColorB": "255", + "FontNoteStyle": "0", + "FontGraphName": ".AppleSystemUIFont", + "FontDefaultName": ".AppleSystemUIFont", + "GraphColorG": "255", + "UseGlobalFileBookmarks": "Y", + "FontDefaultStyle": "0", + "GraphColorB": "255", + "BackgroundColorR": "255", + "BackgroundColorG": "255", + "WorkflowDialogStyle": "RESIZE,MAX,MIN", + "LineWidth": "1", + "ContextDialogShowCategories": "Y" + }, + "projectsConfig": { + "enabled": true, + "projectMandatory": true, + "environmentMandatory": false, + "defaultProject": "default", + "defaultEnvironment": null, + "standardParentProject": "default", + "standardProjectsFolder": null, + "projectConfigurations": [ + { + "projectName": "default", + "projectHome": "${HOP_CONFIG_FOLDER}", + "configFilename": "project-config.json" + } + ], + "lifecycleEnvironments": [ + { + "name": "dev", + "purpose": "Testing", + "projectName": "default", + "configurationFiles": [ + "${PROJECT_HOME}/dev-env-config.json" + ] + } + ], + "projectLifecycles": [] + } +} \ No newline at end of file diff --git a/integration-tests/spark-native/lakehouse/README.md b/integration-tests/spark-native/lakehouse/README.md new file mode 100644 index 00000000000..acc176c0a19 --- /dev/null +++ b/integration-tests/spark-native/lakehouse/README.md @@ -0,0 +1,34 @@ + + +# Lakehouse samples (moved) + +End-to-end integration tests for open table formats live in dedicated projects: + +| Project | Format | +|---------|--------| +| [`integration-tests/lakehouse`](../../lakehouse/) | Delta Lake | +| [`integration-tests/iceberg`](../../iceberg/) | Apache Iceberg | + +Run: + +```bash +./integration-tests/scripts/run-tests.sh lakehouse +./integration-tests/scripts/run-tests.sh iceberg +``` + +See those READMEs for scenarios (Input/Output, overwrite + time travel, MERGE). diff --git a/integration-tests/spark-native/main-0001-input-output.hwf b/integration-tests/spark-native/main-0001-input-output.hwf new file mode 100644 index 00000000000..aeae3b6e2d2 --- /dev/null +++ b/integration-tests/spark-native/main-0001-input-output.hwf @@ -0,0 +1,135 @@ + + + + main-0001-input-output + Y + Native Spark File Input/Output then golden unit test on Local + + + - + 2026/07/15 00:00:00.000 + - + 2026/07/15 00:00:00.000 + + + + Start + + SPECIAL + + N + 0 + 0 + 60 + 12 + 0 + 1 + 1 + N + 64 + 96 + + + + delete output + + DELETE_FILES + + N + N + + + ${PROJECT_HOME}/output/0001-customers-test/ + .* + + + N + 224 + 96 + + + + 0001-input-output.hpl + + PIPELINE + + ${PROJECT_HOME}/0001-input-output.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + + Y + + N + 416 + 96 + + + + Run Pipeline Unit Tests + + RunPipelineTests + + + + 0001-input-output-validation UNIT + + + N + 640 + 96 + + + + + + Start + delete output + Y + Y + Y + + + delete output + 0001-input-output.hpl + Y + Y + N + + + 0001-input-output.hpl + Run Pipeline Unit Tests + Y + Y + N + + + + + diff --git a/integration-tests/spark-native/main-0002-group-by.hwf b/integration-tests/spark-native/main-0002-group-by.hwf new file mode 100644 index 00000000000..0feec6e1050 --- /dev/null +++ b/integration-tests/spark-native/main-0002-group-by.hwf @@ -0,0 +1,135 @@ + + + + main-0002-group-by + Y + Native Spark Memory Group By (by state) then golden unit test on Local + + + - + 2026/07/15 00:00:00.000 + - + 2026/07/15 00:00:00.000 + + + + Start + + SPECIAL + + N + 0 + 0 + 60 + 12 + 0 + 1 + 1 + N + 64 + 96 + + + + delete output + + DELETE_FILES + + N + N + + + ${PROJECT_HOME}/output/0002-group-by/ + .* + + + N + 224 + 96 + + + + 0002-group-by.hpl + + PIPELINE + + ${PROJECT_HOME}/0002-group-by.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + + Y + + N + 416 + 96 + + + + Run Pipeline Unit Tests + + RunPipelineTests + + + + 0002-group-by-validation UNIT + + + N + 640 + 96 + + + + + + Start + delete output + Y + Y + Y + + + delete output + 0002-group-by.hpl + Y + Y + N + + + 0002-group-by.hpl + Run Pipeline Unit Tests + Y + Y + N + + + + + diff --git a/integration-tests/spark-native/main-0003-filter-rows.hwf b/integration-tests/spark-native/main-0003-filter-rows.hwf new file mode 100644 index 00000000000..917e0732f61 --- /dev/null +++ b/integration-tests/spark-native/main-0003-filter-rows.hwf @@ -0,0 +1,135 @@ + + + + main-0003-filter-rows + Y + Native Spark Filter Rows (state STARTS WITH NO) then golden unit test on Local + + + - + 2026/07/15 00:00:00.000 + - + 2026/07/15 00:00:00.000 + + + + Start + + SPECIAL + + N + 0 + 0 + 60 + 12 + 0 + 1 + 1 + N + 64 + 96 + + + + delete output + + DELETE_FILES + + N + N + + + ${PROJECT_HOME}/output/0003-filter-rows/ + .* + + + N + 224 + 96 + + + + 0003-filter-rows.hpl + + PIPELINE + + ${PROJECT_HOME}/0003-filter-rows.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + + Y + + N + 416 + 96 + + + + Run Pipeline Unit Tests + + RunPipelineTests + + + + 0003-filter-rows-validation UNIT + + + N + 640 + 96 + + + + + + Start + delete output + Y + Y + Y + + + delete output + 0003-filter-rows.hpl + Y + Y + N + + + 0003-filter-rows.hpl + Run Pipeline Unit Tests + Y + Y + N + + + + + diff --git a/integration-tests/spark-native/main-0004-stream-lookup.hwf b/integration-tests/spark-native/main-0004-stream-lookup.hwf new file mode 100644 index 00000000000..908efac95e1 --- /dev/null +++ b/integration-tests/spark-native/main-0004-stream-lookup.hwf @@ -0,0 +1,135 @@ + + + + main-0004-stream-lookup + Y + Native Spark Stream Lookup (info stream) then golden unit test on Local + + + - + 2026/07/15 00:00:00.000 + - + 2026/07/15 00:00:00.000 + + + + Start + + SPECIAL + + N + 0 + 0 + 60 + 12 + 0 + 1 + 1 + N + 64 + 96 + + + + delete output + + DELETE_FILES + + N + N + + + ${PROJECT_HOME}/output/0004-stream-lookup/ + .* + + + N + 224 + 96 + + + + 0004-stream-lookup.hpl + + PIPELINE + + ${PROJECT_HOME}/0004-stream-lookup.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + + Y + + N + 416 + 96 + + + + Run Pipeline Unit Tests + + RunPipelineTests + + + + 0004-stream-lookup-validation UNIT + + + N + 640 + 96 + + + + + + Start + delete output + Y + Y + Y + + + delete output + 0004-stream-lookup.hpl + Y + Y + N + + + 0004-stream-lookup.hpl + Run Pipeline Unit Tests + Y + Y + N + + + + + diff --git a/integration-tests/spark-native/main-0005-filter-targets.hwf b/integration-tests/spark-native/main-0005-filter-targets.hwf new file mode 100644 index 00000000000..3ffca95016f --- /dev/null +++ b/integration-tests/spark-native/main-0005-filter-targets.hwf @@ -0,0 +1,139 @@ + + + + main-0005-filter-targets + Y + Native Spark Filter Rows dual targets then golden unit test on Local + + + - + 2026/07/15 00:00:00.000 + - + 2026/07/15 00:00:00.000 + + + + Start + + SPECIAL + + N + 0 + 0 + 60 + 12 + 0 + 1 + 1 + N + 64 + 96 + + + + delete output + + DELETE_FILES + + N + N + + + ${PROJECT_HOME}/output/0005-filter-true/ + .* + + + ${PROJECT_HOME}/output/0005-filter-false/ + .* + + + N + 224 + 96 + + + + 0005-filter-targets.hpl + + PIPELINE + + ${PROJECT_HOME}/0005-filter-targets.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + + Y + + N + 416 + 96 + + + + Run Pipeline Unit Tests + + RunPipelineTests + + + + 0005-filter-targets-validation UNIT + + + N + 640 + 96 + + + + + + Start + delete output + Y + Y + Y + + + delete output + 0005-filter-targets.hpl + Y + Y + N + + + 0005-filter-targets.hpl + Run Pipeline Unit Tests + Y + Y + N + + + + + diff --git a/integration-tests/spark-native/main-0006-switch-case.hwf b/integration-tests/spark-native/main-0006-switch-case.hwf new file mode 100644 index 00000000000..1671fb1ead9 --- /dev/null +++ b/integration-tests/spark-native/main-0006-switch-case.hwf @@ -0,0 +1,109 @@ + + + + main-0006-switch-case + Y + Native Spark Switch/Case target streams then golden unit test on Local + + + - + 2026/07/15 00:00:00.000 + - + 2026/07/15 00:00:00.000 + + + + Start + SPECIAL + + N + 0 + 0 + 60 + 12 + 0 + 1 + 1 + N + 64 + 96 + + + + delete output + DELETE_FILES + + N + N + + ${PROJECT_HOME}/output/0006-switch-ca/.* + ${PROJECT_HOME}/output/0006-switch-fl/.* + ${PROJECT_HOME}/output/0006-switch-ny/.* + ${PROJECT_HOME}/output/0006-switch-default/.* + + N + 224 + 96 + + + + 0006-switch-case.hpl + PIPELINE + + ${PROJECT_HOME}/0006-switch-case.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + Y + N + 416 + 96 + + + + Run Pipeline Unit Tests + RunPipelineTests + + + 0006-switch-case-validation UNIT + + N + 640 + 96 + + + + + Startdelete outputYYY + delete output0006-switch-case.hplYYN + 0006-switch-case.hplRun Pipeline Unit TestsYYN + + + + diff --git a/integration-tests/spark-native/main-0007-text-file-input.hwf b/integration-tests/spark-native/main-0007-text-file-input.hwf new file mode 100644 index 00000000000..cb2a0d71965 --- /dev/null +++ b/integration-tests/spark-native/main-0007-text-file-input.hwf @@ -0,0 +1,177 @@ + + + + New workflow + Y + - + - + 2026/07/16 11:49:49.976 + 2026/07/16 11:49:49.976 + + + + N + 0 + 0 + 60 + 1 + 1 + 0 + 12 + N + Start + + SPECIAL + + 80 + 80 + N + + + + ${PROJECT_HOME}/0007-text-file-input-create-test-files.hpl + N + N + N + N + N + N + N + N + N + Basic + Y + + Y + + local + 0007-text-file-input-create-test-files + + PIPELINE + + 272 + 80 + N + + + + ${PROJECT_HOME}/0007-text-file-input-spark-readback.hpl + N + N + N + N + N + N + N + + + N + N + Basic + Y + + Y + + spark-local + 0007-text-file-input-spark-readback + + PIPELINE + + 560 + 80 + N + + + + ${PROJECT_HOME}/0007-text-file-input-validate.hpl + N + N + N + N + N + N + N + N + N + Basic + Y + + Y + + local + 0007-text-file-input-validate + + PIPELINE + + 816 + 80 + N + + + + + + Start + 0007-text-file-input-create-test-files + Y + Y + Y + + + 0007-text-file-input-create-test-files + 0007-text-file-input-spark-readback + Y + N + Y + + + 0007-text-file-input-spark-readback + 0007-text-file-input-validate + Y + N + Y + + + + + Regular pipeline engine: Create 3 test files in 3 text file output copies +Read the files back with a Spark native engine, write to parquet files. +Verify with an pipeline unit test + Noto Sans + 10 + N + N + 200 + 231 + 250 + 15 + 136 + 210 + 200 + 231 + 250 + 80 + 192 + 32 + 32 + + + + diff --git a/integration-tests/spark-native/main-0008-excel-stream-lookup.hwf b/integration-tests/spark-native/main-0008-excel-stream-lookup.hwf new file mode 100644 index 00000000000..dc08705f2b9 --- /dev/null +++ b/integration-tests/spark-native/main-0008-excel-stream-lookup.hwf @@ -0,0 +1,170 @@ + + + + main-0008-excel-stream-lookup + Y + Native Spark: Excel (info) → Stream Lookup with main from 3 Parquet files + + + - + 2026/07/16 00:00:00.000 + - + 2026/07/16 00:00:00.000 + + + + Start + + SPECIAL + + N + 0 + 0 + 60 + 12 + 0 + 1 + 1 + N + 64 + 96 + + + + delete output + + DELETE_FILES + + N + Y + + + ${PROJECT_HOME}/output/0008-excel-stream-lookup/ + .* + + + N + 208 + 96 + + + + create test files + Local: products.xlsx + 3 order parquet parts + PIPELINE + + ${PROJECT_HOME}/0008-excel-stream-lookup-create-test-files.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + local + + Y + + N + 384 + 96 + + + + 0008-excel-stream-lookup.hpl + Spark-local: Parquet main + Excel info → Stream Lookup + PIPELINE + + ${PROJECT_HOME}/0008-excel-stream-lookup.hpl + N + N + N + N + N + + + N + N + Basic + N + Y + Y + spark-local + + Y + + N + 608 + 96 + + + + Run Pipeline Unit Tests + + RunPipelineTests + + + + 0008-excel-stream-lookup-validation UNIT + + + N + 848 + 96 + + + + + + Start + delete output + Y + Y + Y + + + delete output + create test files + Y + Y + N + + + create test files + 0008-excel-stream-lookup.hpl + Y + Y + N + + + 0008-excel-stream-lookup.hpl + Run Pipeline Unit Tests + Y + Y + N + + + + + diff --git a/integration-tests/spark-native/metadata/dataset/0001-customers-1k-golden.json b/integration-tests/spark-native/metadata/dataset/0001-customers-1k-golden.json new file mode 100644 index 00000000000..989a693221d --- /dev/null +++ b/integration-tests/spark-native/metadata/dataset/0001-customers-1k-golden.json @@ -0,0 +1,88 @@ +{ + "base_filename": "0001-customers-1k-golden.csv", + "name": "0001-customers-1k-golden", + "description": "Golden customers-1k after Spark File Input/Output (trimmed values, all 10 columns)", + "dataset_fields": [ + { + "field_comment": "", + "field_length": -1, + "field_type": 5, + "field_precision": 0, + "field_name": "id", + "field_format": "#" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "name", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "firstname", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "zip", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "city", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "birthdate", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "street", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "housenr", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "stateCode", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "state", + "field_format": "" + } + ], + "folder_name": "" +} \ No newline at end of file diff --git a/integration-tests/spark-native/metadata/dataset/0002-group-by-golden.json b/integration-tests/spark-native/metadata/dataset/0002-group-by-golden.json new file mode 100644 index 00000000000..fae925d4c02 --- /dev/null +++ b/integration-tests/spark-native/metadata/dataset/0002-group-by-golden.json @@ -0,0 +1,56 @@ +{ + "base_filename": "0002-group-by-golden.csv", + "name": "0002-group-by-golden", + "description": "Golden Memory Group By by state: count_all, count_distinct_city, min_id, max_id, sum_id", + "dataset_fields": [ + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "state", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 5, + "field_precision": 0, + "field_name": "count_all", + "field_format": "#" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 5, + "field_precision": 0, + "field_name": "count_distinct_city", + "field_format": "#" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 5, + "field_precision": 0, + "field_name": "min_id", + "field_format": "#" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 5, + "field_precision": 0, + "field_name": "max_id", + "field_format": "#" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 5, + "field_precision": 0, + "field_name": "sum_id", + "field_format": "#" + } + ], + "folder_name": "" +} diff --git a/integration-tests/spark-native/metadata/dataset/0003-filter-rows-golden.json b/integration-tests/spark-native/metadata/dataset/0003-filter-rows-golden.json new file mode 100644 index 00000000000..da02bd21a22 --- /dev/null +++ b/integration-tests/spark-native/metadata/dataset/0003-filter-rows-golden.json @@ -0,0 +1,88 @@ +{ + "base_filename": "0003-filter-rows-golden.csv", + "name": "0003-filter-rows-golden", + "description": "Golden Filter Rows: customers-1k where state STARTS WITH NO (47 rows)", + "dataset_fields": [ + { + "field_comment": "", + "field_length": -1, + "field_type": 5, + "field_precision": 0, + "field_name": "id", + "field_format": "#" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "name", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "firstname", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "zip", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "city", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "birthdate", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "street", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "housenr", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "stateCode", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "state", + "field_format": "" + } + ], + "folder_name": "" +} diff --git a/integration-tests/spark-native/metadata/dataset/0004-stream-lookup-golden.json b/integration-tests/spark-native/metadata/dataset/0004-stream-lookup-golden.json new file mode 100644 index 00000000000..54f0bac7e04 --- /dev/null +++ b/integration-tests/spark-native/metadata/dataset/0004-stream-lookup-golden.json @@ -0,0 +1,96 @@ +{ + "base_filename": "0004-stream-lookup-golden.csv", + "name": "0004-stream-lookup-golden", + "description": "Golden Stream Lookup: customers-1k with countPerState by stateCode", + "dataset_fields": [ + { + "field_comment": "", + "field_length": -1, + "field_type": 5, + "field_precision": 0, + "field_name": "id", + "field_format": "#" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "name", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "firstname", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "zip", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "city", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "birthdate", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "street", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "housenr", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "stateCode", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "state", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 5, + "field_precision": 0, + "field_name": "countPerState", + "field_format": "#" + } + ], + "folder_name": "" +} diff --git a/integration-tests/spark-native/metadata/dataset/0005-filter-false-golden.json b/integration-tests/spark-native/metadata/dataset/0005-filter-false-golden.json new file mode 100644 index 00000000000..1f37b9b1b89 --- /dev/null +++ b/integration-tests/spark-native/metadata/dataset/0005-filter-false-golden.json @@ -0,0 +1,32 @@ +{ + "base_filename": "0005-filter-false-golden.csv", + "name": "0005-filter-false-golden", + "description": "Golden Filter dual-target false branch (953 rows)", + "dataset_fields": [ + { + "field_comment": "", + "field_length": -1, + "field_type": 5, + "field_precision": 0, + "field_name": "id", + "field_format": "#" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "stateCode", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "state", + "field_format": "" + } + ], + "folder_name": "" +} diff --git a/integration-tests/spark-native/metadata/dataset/0005-filter-true-golden.json b/integration-tests/spark-native/metadata/dataset/0005-filter-true-golden.json new file mode 100644 index 00000000000..1fd6fca614d --- /dev/null +++ b/integration-tests/spark-native/metadata/dataset/0005-filter-true-golden.json @@ -0,0 +1,32 @@ +{ + "base_filename": "0005-filter-true-golden.csv", + "name": "0005-filter-true-golden", + "description": "Golden Filter dual-target true branch state STARTS WITH NO (47 rows)", + "dataset_fields": [ + { + "field_comment": "", + "field_length": -1, + "field_type": 5, + "field_precision": 0, + "field_name": "id", + "field_format": "#" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "stateCode", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "state", + "field_format": "" + } + ], + "folder_name": "" +} diff --git a/integration-tests/spark-native/metadata/dataset/0006-switch-ca-golden.json b/integration-tests/spark-native/metadata/dataset/0006-switch-ca-golden.json new file mode 100644 index 00000000000..85f7cb40fb9 --- /dev/null +++ b/integration-tests/spark-native/metadata/dataset/0006-switch-ca-golden.json @@ -0,0 +1,24 @@ +{ + "base_filename": "0006-switch-ca-golden.csv", + "name": "0006-switch-ca-golden", + "description": "Golden Switch/Case CA branch (19 rows)", + "dataset_fields": [ + { + "field_comment": "", + "field_length": -1, + "field_type": 5, + "field_precision": 0, + "field_name": "id", + "field_format": "#" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "stateCode", + "field_format": "" + } + ], + "folder_name": "" +} diff --git a/integration-tests/spark-native/metadata/dataset/0008-excel-stream-lookup-golden.json b/integration-tests/spark-native/metadata/dataset/0008-excel-stream-lookup-golden.json new file mode 100644 index 00000000000..6e29597a5ac --- /dev/null +++ b/integration-tests/spark-native/metadata/dataset/0008-excel-stream-lookup-golden.json @@ -0,0 +1,48 @@ +{ + "base_filename": "0008-excel-stream-lookup-golden.csv", + "name": "0008-excel-stream-lookup-golden", + "description": "Golden: 6 orders from 3 parquet parts with productName/unitPrice from Excel via Stream Lookup", + "dataset_fields": [ + { + "field_comment": "", + "field_length": -1, + "field_type": 5, + "field_precision": 0, + "field_name": "orderId", + "field_format": "#" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "productCode", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 5, + "field_precision": 0, + "field_name": "qty", + "field_format": "#" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "productName", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 5, + "field_precision": 0, + "field_name": "unitPrice", + "field_format": "#" + } + ], + "folder_name": "" +} diff --git a/integration-tests/spark-native/metadata/dataset/0099-complex-golden.json b/integration-tests/spark-native/metadata/dataset/0099-complex-golden.json new file mode 100644 index 00000000000..68871060138 --- /dev/null +++ b/integration-tests/spark-native/metadata/dataset/0099-complex-golden.json @@ -0,0 +1,128 @@ +{ + "base_filename": "0099-complex-golden.csv", + "name": "0099-complex-golden", + "description": "", + "dataset_fields": [ + { + "field_comment": "", + "field_length": 15, + "field_type": 5, + "field_precision": 0, + "field_name": "id", + "field_format": "" + }, + { + "field_comment": "", + "field_length": 15, + "field_type": 2, + "field_precision": -1, + "field_name": "Last name", + "field_format": "" + }, + { + "field_comment": "", + "field_length": 20, + "field_type": 2, + "field_precision": -1, + "field_name": "First name", + "field_format": "" + }, + { + "field_comment": "", + "field_length": 15, + "field_type": 2, + "field_precision": -1, + "field_name": "cust_zip_code", + "field_format": "" + }, + { + "field_comment": "", + "field_length": 8, + "field_type": 2, + "field_precision": -1, + "field_name": "city", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "birthdate", + "field_format": "" + }, + { + "field_comment": "", + "field_length": 11, + "field_type": 2, + "field_precision": -1, + "field_name": "street", + "field_format": "" + }, + { + "field_comment": "", + "field_length": 15, + "field_type": 2, + "field_precision": -1, + "field_name": "housenr", + "field_format": "" + }, + { + "field_comment": "", + "field_length": 9, + "field_type": 2, + "field_precision": -1, + "field_name": "stateCode", + "field_format": "" + }, + { + "field_comment": "", + "field_length": 30, + "field_type": 2, + "field_precision": -1, + "field_name": "state", + "field_format": "" + }, + { + "field_comment": "", + "field_length": 100, + "field_type": 2, + "field_precision": -1, + "field_name": "state_1", + "field_format": "" + }, + { + "field_comment": "", + "field_length": 9, + "field_type": 5, + "field_precision": 0, + "field_name": "population", + "field_format": "" + }, + { + "field_comment": "", + "field_length": 10, + "field_type": 5, + "field_precision": 0, + "field_name": "nrPerState", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "label", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "Comment", + "field_format": "" + } + ], + "folder_name": "" +} \ No newline at end of file diff --git a/integration-tests/spark-native/metadata/dataset/0099-customers-input.json b/integration-tests/spark-native/metadata/dataset/0099-customers-input.json new file mode 100644 index 00000000000..d7e95dac36e --- /dev/null +++ b/integration-tests/spark-native/metadata/dataset/0099-customers-input.json @@ -0,0 +1,88 @@ +{ + "base_filename": "0099-customers-input.csv", + "name": "0099-customers-input", + "description": "", + "dataset_fields": [ + { + "field_comment": "", + "field_length": 15, + "field_type": 5, + "field_precision": 0, + "field_name": "id", + "field_format": "" + }, + { + "field_comment": "", + "field_length": 15, + "field_type": 2, + "field_precision": -1, + "field_name": "Last name", + "field_format": "" + }, + { + "field_comment": "", + "field_length": 20, + "field_type": 2, + "field_precision": -1, + "field_name": "First name", + "field_format": "" + }, + { + "field_comment": "", + "field_length": 15, + "field_type": 2, + "field_precision": 0, + "field_name": "cust_zip_code", + "field_format": "" + }, + { + "field_comment": "", + "field_length": 8, + "field_type": 2, + "field_precision": -1, + "field_name": "city", + "field_format": "" + }, + { + "field_comment": "", + "field_length": -1, + "field_type": 2, + "field_precision": -1, + "field_name": "birthdate", + "field_format": "" + }, + { + "field_comment": "", + "field_length": 11, + "field_type": 2, + "field_precision": -1, + "field_name": "street", + "field_format": "" + }, + { + "field_comment": "", + "field_length": 15, + "field_type": 2, + "field_precision": 0, + "field_name": "housenr", + "field_format": "" + }, + { + "field_comment": "", + "field_length": 9, + "field_type": 2, + "field_precision": -1, + "field_name": "stateCode", + "field_format": "" + }, + { + "field_comment": "", + "field_length": 30, + "field_type": 2, + "field_precision": -1, + "field_name": "state", + "field_format": "" + } + ], + "folder_name": "" +} \ No newline at end of file diff --git a/integration-tests/spark-native/metadata/dataset/0099-state-population-input.json b/integration-tests/spark-native/metadata/dataset/0099-state-population-input.json new file mode 100644 index 00000000000..a9b290f7889 --- /dev/null +++ b/integration-tests/spark-native/metadata/dataset/0099-state-population-input.json @@ -0,0 +1,24 @@ +{ + "base_filename": "0099-state-data-input.csv", + "name": "0099-state-population-input", + "description": "", + "dataset_fields": [ + { + "field_comment": "", + "field_length": 100, + "field_type": 2, + "field_precision": -1, + "field_name": "state", + "field_format": "" + }, + { + "field_comment": "", + "field_length": 9, + "field_type": 5, + "field_precision": 0, + "field_name": "population", + "field_format": "#" + } + ], + "folder_name": "" +} \ No newline at end of file diff --git a/integration-tests/spark-native/metadata/pipeline-run-configuration/local.json b/integration-tests/spark-native/metadata/pipeline-run-configuration/local.json new file mode 100644 index 00000000000..d8e37459d83 --- /dev/null +++ b/integration-tests/spark-native/metadata/pipeline-run-configuration/local.json @@ -0,0 +1,17 @@ +{ + "engineRunConfiguration": { + "Local": { + "feedback_size": "50000", + "sample_size": "100", + "sample_type_in_gui": "Last", + "rowset_size": "10000", + "safe_mode": false, + "show_feedback": false, + "topo_sort": false, + "gather_metrics": false + } + }, + "configurationVariables": [], + "name": "local", + "description": "Runs your pipelines locally with the standard local Hop pipeline engine" +} diff --git a/integration-tests/spark-native/metadata/pipeline-run-configuration/spark-local.json b/integration-tests/spark-native/metadata/pipeline-run-configuration/spark-local.json new file mode 100644 index 00000000000..95f7e28412b --- /dev/null +++ b/integration-tests/spark-native/metadata/pipeline-run-configuration/spark-local.json @@ -0,0 +1,18 @@ +{ + "engineRunConfiguration": { + "SparkPipelineEngine": { + "sparkMaster": "local[2]", + "sparkAppName": "hop-it-spark-native", + "sparkConfigs": "spark.ui.enabled=false\nspark.driver.host=localhost", + "fatJar": "", + "tempLocation": "/tmp", + "driverMemory": "", + "executorMemory": "", + "executorCores": "", + "pluginsToStage": "" + } + }, + "configurationVariables": [], + "name": "spark-local", + "description": "Native SparkPipelineEngine embedded local[2]. Leave fatJar empty for local sessions (Spark 4.1 plugin classpath). hop-beam-image still provides /tmp/hop-fatjar.jar for future native-cluster IT; do not set fatJar to that Beam 3.5 pack jar until a native 4.x pack exists." +} diff --git a/integration-tests/spark-native/metadata/unit-test/0001-input-output-validation UNIT.json b/integration-tests/spark-native/metadata/unit-test/0001-input-output-validation UNIT.json new file mode 100644 index 00000000000..2ca618607e7 --- /dev/null +++ b/integration-tests/spark-native/metadata/unit-test/0001-input-output-validation UNIT.json @@ -0,0 +1,32 @@ +{ + "variableValues": [], + "database_replacements": [], + "autoOpening": true, + "basePath": "", + "golden_data_sets": [ + { + "field_mappings": [ + { "transform_field": "id", "data_set_field": "id" }, + { "transform_field": "name", "data_set_field": "name" }, + { "transform_field": "firstname", "data_set_field": "firstname" }, + { "transform_field": "zip", "data_set_field": "zip" }, + { "transform_field": "city", "data_set_field": "city" }, + { "transform_field": "birthdate", "data_set_field": "birthdate" }, + { "transform_field": "street", "data_set_field": "street" }, + { "transform_field": "housenr", "data_set_field": "housenr" }, + { "transform_field": "stateCode", "data_set_field": "stateCode" }, + { "transform_field": "state", "data_set_field": "state" } + ], + "field_order": ["id"], + "data_set_name": "0001-customers-1k-golden", + "transform_name": "Verify" + } + ], + "input_data_sets": [], + "name": "0001-input-output-validation UNIT", + "description": "Compare Spark File Output CSV (read back on Local) to golden customers-1k", + "persist_filename": "", + "trans_test_tweaks": [], + "pipeline_filename": "./0001-input-output-validation.hpl", + "test_type": "UNIT_TEST" +} diff --git a/integration-tests/spark-native/metadata/unit-test/0002-group-by-validation UNIT.json b/integration-tests/spark-native/metadata/unit-test/0002-group-by-validation UNIT.json new file mode 100644 index 00000000000..972c27f4ca7 --- /dev/null +++ b/integration-tests/spark-native/metadata/unit-test/0002-group-by-validation UNIT.json @@ -0,0 +1,28 @@ +{ + "variableValues": [], + "database_replacements": [], + "autoOpening": true, + "basePath": "", + "golden_data_sets": [ + { + "field_mappings": [ + { "transform_field": "state", "data_set_field": "state" }, + { "transform_field": "count_all", "data_set_field": "count_all" }, + { "transform_field": "count_distinct_city", "data_set_field": "count_distinct_city" }, + { "transform_field": "min_id", "data_set_field": "min_id" }, + { "transform_field": "max_id", "data_set_field": "max_id" }, + { "transform_field": "sum_id", "data_set_field": "sum_id" } + ], + "field_order": ["state"], + "data_set_name": "0002-group-by-golden", + "transform_name": "Verify" + } + ], + "input_data_sets": [], + "name": "0002-group-by-validation UNIT", + "description": "Compare Spark Memory Group By output (read back on Local) to golden aggregates by state", + "persist_filename": "", + "trans_test_tweaks": [], + "pipeline_filename": "./0002-group-by-validation.hpl", + "test_type": "UNIT_TEST" +} diff --git a/integration-tests/spark-native/metadata/unit-test/0003-filter-rows-validation UNIT.json b/integration-tests/spark-native/metadata/unit-test/0003-filter-rows-validation UNIT.json new file mode 100644 index 00000000000..152cfd7e79e --- /dev/null +++ b/integration-tests/spark-native/metadata/unit-test/0003-filter-rows-validation UNIT.json @@ -0,0 +1,32 @@ +{ + "variableValues": [], + "database_replacements": [], + "autoOpening": true, + "basePath": "", + "golden_data_sets": [ + { + "field_mappings": [ + { "transform_field": "id", "data_set_field": "id" }, + { "transform_field": "name", "data_set_field": "name" }, + { "transform_field": "firstname", "data_set_field": "firstname" }, + { "transform_field": "zip", "data_set_field": "zip" }, + { "transform_field": "city", "data_set_field": "city" }, + { "transform_field": "birthdate", "data_set_field": "birthdate" }, + { "transform_field": "street", "data_set_field": "street" }, + { "transform_field": "housenr", "data_set_field": "housenr" }, + { "transform_field": "stateCode", "data_set_field": "stateCode" }, + { "transform_field": "state", "data_set_field": "state" } + ], + "field_order": ["id"], + "data_set_name": "0003-filter-rows-golden", + "transform_name": "Verify" + } + ], + "input_data_sets": [], + "name": "0003-filter-rows-validation UNIT", + "description": "Compare Spark Filter Rows output (state STARTS WITH NO) to golden", + "persist_filename": "", + "trans_test_tweaks": [], + "pipeline_filename": "./0003-filter-rows-validation.hpl", + "test_type": "UNIT_TEST" +} diff --git a/integration-tests/spark-native/metadata/unit-test/0004-stream-lookup-validation UNIT.json b/integration-tests/spark-native/metadata/unit-test/0004-stream-lookup-validation UNIT.json new file mode 100644 index 00000000000..5f85ae5658b --- /dev/null +++ b/integration-tests/spark-native/metadata/unit-test/0004-stream-lookup-validation UNIT.json @@ -0,0 +1,33 @@ +{ + "variableValues": [], + "database_replacements": [], + "autoOpening": true, + "basePath": "", + "golden_data_sets": [ + { + "field_mappings": [ + { "transform_field": "id", "data_set_field": "id" }, + { "transform_field": "name", "data_set_field": "name" }, + { "transform_field": "firstname", "data_set_field": "firstname" }, + { "transform_field": "zip", "data_set_field": "zip" }, + { "transform_field": "city", "data_set_field": "city" }, + { "transform_field": "birthdate", "data_set_field": "birthdate" }, + { "transform_field": "street", "data_set_field": "street" }, + { "transform_field": "housenr", "data_set_field": "housenr" }, + { "transform_field": "stateCode", "data_set_field": "stateCode" }, + { "transform_field": "state", "data_set_field": "state" }, + { "transform_field": "countPerState", "data_set_field": "countPerState" } + ], + "field_order": ["id"], + "data_set_name": "0004-stream-lookup-golden", + "transform_name": "Verify" + } + ], + "input_data_sets": [], + "name": "0004-stream-lookup-validation UNIT", + "description": "Compare Spark Stream Lookup output to golden counts per stateCode", + "persist_filename": "", + "trans_test_tweaks": [], + "pipeline_filename": "./0004-stream-lookup-validation.hpl", + "test_type": "UNIT_TEST" +} diff --git a/integration-tests/spark-native/metadata/unit-test/0005-filter-targets-validation UNIT.json b/integration-tests/spark-native/metadata/unit-test/0005-filter-targets-validation UNIT.json new file mode 100644 index 00000000000..cf84114e6a9 --- /dev/null +++ b/integration-tests/spark-native/metadata/unit-test/0005-filter-targets-validation UNIT.json @@ -0,0 +1,57 @@ +{ + "variableValues": [], + "database_replacements": [], + "autoOpening": true, + "basePath": "", + "golden_data_sets": [ + { + "field_mappings": [ + { + "transform_field": "id", + "data_set_field": "id" + }, + { + "transform_field": "stateCode", + "data_set_field": "stateCode" + }, + { + "transform_field": "state", + "data_set_field": "state" + } + ], + "field_order": [ + "id" + ], + "data_set_name": "0005-filter-true-golden", + "transform_name": "Verify true" + }, + { + "field_mappings": [ + { + "transform_field": "id", + "data_set_field": "id" + }, + { + "transform_field": "stateCode", + "data_set_field": "stateCode" + }, + { + "transform_field": "state", + "data_set_field": "state" + } + ], + "field_order": [ + "id" + ], + "data_set_name": "0005-filter-false-golden", + "transform_name": "Verify false" + } + ], + "input_data_sets": [], + "name": "0005-filter-targets-validation UNIT", + "description": "Compare dual Filter Rows target outputs to golden", + "persist_filename": "", + "trans_test_tweaks": [], + "pipeline_filename": "./0005-filter-targets-validation.hpl", + "test_type": "UNIT_TEST" +} diff --git a/integration-tests/spark-native/metadata/unit-test/0006-switch-case-validation UNIT.json b/integration-tests/spark-native/metadata/unit-test/0006-switch-case-validation UNIT.json new file mode 100644 index 00000000000..f20fa7c5bd3 --- /dev/null +++ b/integration-tests/spark-native/metadata/unit-test/0006-switch-case-validation UNIT.json @@ -0,0 +1,32 @@ +{ + "variableValues": [], + "database_replacements": [], + "autoOpening": true, + "basePath": "", + "golden_data_sets": [ + { + "field_mappings": [ + { + "transform_field": "id", + "data_set_field": "id" + }, + { + "transform_field": "stateCode", + "data_set_field": "stateCode" + } + ], + "field_order": [ + "id" + ], + "data_set_name": "0006-switch-ca-golden", + "transform_name": "Verify" + } + ], + "input_data_sets": [], + "name": "0006-switch-case-validation UNIT", + "description": "Compare Switch/Case CA target output to golden", + "persist_filename": "", + "trans_test_tweaks": [], + "pipeline_filename": "./0006-switch-case-validation.hpl", + "test_type": "UNIT_TEST" +} diff --git a/integration-tests/spark-native/metadata/unit-test/0008-excel-stream-lookup-validation UNIT.json b/integration-tests/spark-native/metadata/unit-test/0008-excel-stream-lookup-validation UNIT.json new file mode 100644 index 00000000000..a37183e8113 --- /dev/null +++ b/integration-tests/spark-native/metadata/unit-test/0008-excel-stream-lookup-validation UNIT.json @@ -0,0 +1,27 @@ +{ + "variableValues": [], + "database_replacements": [], + "autoOpening": true, + "basePath": "", + "golden_data_sets": [ + { + "field_mappings": [ + { "transform_field": "orderId", "data_set_field": "orderId" }, + { "transform_field": "productCode", "data_set_field": "productCode" }, + { "transform_field": "qty", "data_set_field": "qty" }, + { "transform_field": "productName", "data_set_field": "productName" }, + { "transform_field": "unitPrice", "data_set_field": "unitPrice" } + ], + "field_order": ["orderId"], + "data_set_name": "0008-excel-stream-lookup-golden", + "transform_name": "Verify" + } + ], + "input_data_sets": [], + "name": "0008-excel-stream-lookup-validation UNIT", + "description": "Compare Spark Excel+Stream Lookup output to golden orders with product attributes", + "persist_filename": "", + "trans_test_tweaks": [], + "pipeline_filename": "./0008-excel-stream-lookup-validation.hpl", + "test_type": "UNIT_TEST" +} diff --git a/integration-tests/spark-native/metadata/unit-test/0099-complex UNIT.json b/integration-tests/spark-native/metadata/unit-test/0099-complex UNIT.json new file mode 100644 index 00000000000..3766b0a2be0 --- /dev/null +++ b/integration-tests/spark-native/metadata/unit-test/0099-complex UNIT.json @@ -0,0 +1,152 @@ +{ + "database_replacements": [], + "autoOpening": true, + "description": "", + "persist_filename": "", + "test_type": "UNIT_TEST", + "variableValues": [], + "basePath": "${HOP_UNIT_TESTS_FOLDER}", + "golden_data_sets": [ + { + "field_mappings": [ + { + "transform_field": "id", + "data_set_field": "id" + }, + { + "transform_field": "name", + "data_set_field": "Last name" + }, + { + "transform_field": "firstname", + "data_set_field": "First name" + }, + { + "transform_field": "zip", + "data_set_field": "cust_zip_code" + }, + { + "transform_field": "city", + "data_set_field": "city" + }, + { + "transform_field": "birthdate", + "data_set_field": "birthdate" + }, + { + "transform_field": "street", + "data_set_field": "street" + }, + { + "transform_field": "housenr", + "data_set_field": "housenr" + }, + { + "transform_field": "stateCode", + "data_set_field": "stateCode" + }, + { + "transform_field": "state", + "data_set_field": "state" + }, + { + "transform_field": "state_1", + "data_set_field": "state_1" + }, + { + "transform_field": "count", + "data_set_field": "population" + }, + { + "transform_field": "nrPerState", + "data_set_field": "nrPerState" + }, + { + "transform_field": "label", + "data_set_field": "label" + }, + { + "transform_field": "Comment", + "data_set_field": "Comment" + } + ], + "field_order": [ + "id" + ], + "data_set_name": "0099-complex-golden", + "transform_name": "output/0099-complex.csv" + } + ], + "input_data_sets": [ + { + "field_mappings": [ + { + "transform_field": "id", + "data_set_field": "id" + }, + { + "transform_field": "name", + "data_set_field": "Last name" + }, + { + "transform_field": "firstname", + "data_set_field": "First name" + }, + { + "transform_field": "zip", + "data_set_field": "cust_zip_code" + }, + { + "transform_field": "city", + "data_set_field": "city" + }, + { + "transform_field": "birthdate", + "data_set_field": "birthdate" + }, + { + "transform_field": "street", + "data_set_field": "street" + }, + { + "transform_field": "housenr", + "data_set_field": "housenr" + }, + { + "transform_field": "stateCode", + "data_set_field": "stateCode" + }, + { + "transform_field": "state", + "data_set_field": "state" + } + ], + "field_order": [ + "id" + ], + "data_set_name": "0099-customers-input", + "transform_name": "files/customers-noheader-1k.txt" + }, + { + "field_mappings": [ + { + "transform_field": "state", + "data_set_field": "state" + }, + { + "transform_field": "count", + "data_set_field": "population" + } + ], + "field_order": [ + "state", + "population" + ], + "data_set_name": "0099-state-population-input", + "transform_name": "files/state-population.txt" + } + ], + "name": "0099-complex UNIT", + "trans_test_tweaks": [], + "pipeline_filename": "./0099-complex.hpl" +} \ No newline at end of file diff --git a/integration-tests/spark-native/metadata/workflow-run-configuration/local.json b/integration-tests/spark-native/metadata/workflow-run-configuration/local.json new file mode 100644 index 00000000000..ddb388679aa --- /dev/null +++ b/integration-tests/spark-native/metadata/workflow-run-configuration/local.json @@ -0,0 +1,9 @@ +{ + "engineRunConfiguration": { + "Local": { + "safe_mode": false + } + }, + "name": "local", + "description": "Runs your workflows locally with the standard local Hop workflow engine" +} diff --git a/integration-tests/spark-native/project-config.json b/integration-tests/spark-native/project-config.json new file mode 100644 index 00000000000..6a91171e1c8 --- /dev/null +++ b/integration-tests/spark-native/project-config.json @@ -0,0 +1,13 @@ +{ + "metadataBaseFolder" : "${PROJECT_HOME}/metadata", + "unitTestsBasePath" : "${PROJECT_HOME}", + "dataSetsCsvFolder" : "${PROJECT_HOME}/datasets", + "enforcingExecutionInHome" : true, + "config" : { + "variables" : [ { + "name" : "HOP_LICENSE_HEADER_FILE", + "value" : "${PROJECT_HOME}/../asf-header.txt", + "description" : "This will automatically serialize the ASF license header into pipelines and workflows in the integration test projects" + } ] + } +} \ No newline at end of file diff --git a/integration-tests/transforms/0065-check-excel-file-exists.hpl b/integration-tests/transforms/0065-check-excel-file-exists.hpl index 5b54be5913a..aabe6484c5f 100644 --- a/integration-tests/transforms/0065-check-excel-file-exists.hpl +++ b/integration-tests/transforms/0065-check-excel-file-exists.hpl @@ -153,7 +153,7 @@ limitations under the License. N N - ${PROJECT_HOME}/files/excel/temp-excel-output.xlsx + ${PROJECT_HOME}/output/temp-excel-output.xlsx N diff --git a/integration-tests/transforms/0065-create-excel-file.hpl b/integration-tests/transforms/0065-create-excel-file.hpl index dda0d7c02e5..1e534d784a9 100644 --- a/integration-tests/transforms/0065-create-excel-file.hpl +++ b/integration-tests/transforms/0065-create-excel-file.hpl @@ -118,7 +118,7 @@ limitations under the License. N new new - ${PROJECT_HOME}/files/excel/temp-excel-output + ${PROJECT_HOME}/output/temp-excel-output N diff --git a/integration-tests/transforms/0101-check-ods-file-exists.hpl b/integration-tests/transforms/0101-check-ods-file-exists.hpl index 93c7a8e0d85..a291f1a76a2 100644 --- a/integration-tests/transforms/0101-check-ods-file-exists.hpl +++ b/integration-tests/transforms/0101-check-ods-file-exists.hpl @@ -153,7 +153,7 @@ limitations under the License. N N - ${PROJECT_HOME}/files/excel/temp-ods-output.ods + ${PROJECT_HOME}/output/temp-ods-output.ods N diff --git a/integration-tests/transforms/0101-create-ods-file.hpl b/integration-tests/transforms/0101-create-ods-file.hpl index 55ec0ba6037..a524ef6ed29 100644 --- a/integration-tests/transforms/0101-create-ods-file.hpl +++ b/integration-tests/transforms/0101-create-ods-file.hpl @@ -118,7 +118,7 @@ limitations under the License. N new new - ${PROJECT_HOME}/files/excel/temp-ods-output + ${PROJECT_HOME}/output/temp-ods-output N diff --git a/integration-tests/transforms/output/.gitignore b/integration-tests/transforms/output/.gitignore new file mode 100644 index 00000000000..2a246d87981 --- /dev/null +++ b/integration-tests/transforms/output/.gitignore @@ -0,0 +1,4 @@ +# Temp files written by integration tests (Excel writer, etc.) +* +!.gitignore +!.gitkeep diff --git a/integration-tests/transforms/output/.gitkeep b/integration-tests/transforms/output/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/plugins/engines/beam/src/main/java/org/apache/hop/beam/config/GenerateFatJarConfigPlugin.java b/plugins/engines/beam/src/main/java/org/apache/hop/beam/config/GenerateFatJarConfigPlugin.java index 52586c5c3a8..2329ebf646a 100644 --- a/plugins/engines/beam/src/main/java/org/apache/hop/beam/config/GenerateFatJarConfigPlugin.java +++ b/plugins/engines/beam/src/main/java/org/apache/hop/beam/config/GenerateFatJarConfigPlugin.java @@ -44,9 +44,11 @@ public class GenerateFatJarConfigPlugin implements IConfigOptions { @CommandLine.Option( names = {"-scv", "--spark-client-version"}, description = - "Spark client pack version to embed (directory lib/spark-clients/). " + "Spark client pack version to embed (directory lib/spark-clients/), " + + "or special tokens: 'native' (embed Spark 4 from plugins/engines/spark/lib for hop-run), " + + "'native-provided' (exclude Spark/Scala — use with spark-submit; cluster provides Spark). " + "When omitted, uses HOP_SPARK_CLIENT_VERSION or the default pack at lib/spark-client. " - + "Client and Spark cluster minor versions must match for client-mode submit.") + + "For Beam packs, client and Spark cluster minor versions must match for client-mode submit.") private String sparkClientVersion; @Override @@ -74,21 +76,49 @@ private void createFatJar(ILogChannel log, IVariables variables) throws HopExcep StringUtils.isNotEmpty(sparkClientVersion) ? variables.resolve(sparkClientVersion) : null); - File packDir = HopBeamGuiPlugin.resolveSparkClientPackDir(resolvedSparkClientVersion); - if (StringUtils.isNotBlank(resolvedSparkClientVersion)) { - log.logBasic( - "Using Spark client pack version " - + resolvedSparkClientVersion - + " from " - + packDir.getPath()); - if (!packDir.isDirectory()) { + + if (HopBeamGuiPlugin.isNativeSparkClientVersion(resolvedSparkClientVersion)) { + boolean provided = + HopBeamGuiPlugin.isNativeProvidedSparkClientVersion(resolvedSparkClientVersion); + if (provided) { + log.logBasic( + "Native Spark fat jar mode (native-provided): excluding Beam Spark 3 / Scala 2.12 " + + "and all Spark/Scala runtime jars. Use with spark-submit — cluster provides Spark."); + } else { + log.logBasic( + "Native Spark 4 fat jar mode (native): excluding Beam Spark 3 client pack and Scala " + + "2.12 jars; including plugins/engines/spark/lib (Spark 4.x / Scala 2.13). " + + "For spark-submit prefer --spark-client-version=native-provided."); + } + File nativeSparkPlugin = new File("plugins/engines/spark"); + if (!nativeSparkPlugin.isDirectory()) { throw new HopException( - "Spark client pack not found: " - + packDir.getPath() - + " — materialise it with tools/spark-client-pack/materialize-pack.sh"); + "Native Spark engine plugin not found at " + + nativeSparkPlugin.getPath() + + " — install plugins/engines/spark (with lib/) before generating a native fat jar"); + } + File nativeLib = new File(nativeSparkPlugin, "lib"); + if (!nativeLib.isDirectory() && !provided) { + log.logBasic( + "WARNING: plugins/engines/spark/lib is missing; fat jar may lack Spark 4 runtime jars"); } } else { - log.logBasic("Using default Spark client pack from " + packDir.getPath()); + File packDir = HopBeamGuiPlugin.resolveSparkClientPackDir(resolvedSparkClientVersion); + if (StringUtils.isNotBlank(resolvedSparkClientVersion)) { + log.logBasic( + "Using Spark client pack version " + + resolvedSparkClientVersion + + " from " + + packDir.getPath()); + if (!packDir.isDirectory()) { + throw new HopException( + "Spark client pack not found: " + + packDir.getPath() + + " — materialise it with tools/spark-client-pack/materialize-pack.sh"); + } + } else { + log.logBasic("Using default Spark client pack from " + packDir.getPath()); + } } List installedJarFilenames = diff --git a/plugins/engines/beam/src/main/java/org/apache/hop/beam/gui/HopBeamGuiPlugin.java b/plugins/engines/beam/src/main/java/org/apache/hop/beam/gui/HopBeamGuiPlugin.java index 02e01b7c87f..c6119d456e6 100644 --- a/plugins/engines/beam/src/main/java/org/apache/hop/beam/gui/HopBeamGuiPlugin.java +++ b/plugins/engines/beam/src/main/java/org/apache/hop/beam/gui/HopBeamGuiPlugin.java @@ -68,6 +68,20 @@ public class HopBeamGuiPlugin { public static final String TOOLBAR_ID_PIPELINE_EXECUTION_VIEWER_VISIT_GCP_DATAFLOW = "PipelineExecutionViewer-Toolbar-20000-VisitGcpDataflow"; + /** + * Special {@code --spark-client-version} token for a native Spark 4 fat jar: exclude Beam's Spark + * 3 / Scala 2.12 client classpath and keep {@code plugins/engines/spark/lib} (Spark 4 embedded). + * Suitable for hop-run client mode; for spark-submit use {@link + * #SPARK_CLIENT_VERSION_NATIVE_PROVIDED}. + */ + public static final String SPARK_CLIENT_VERSION_NATIVE = "native"; + + /** + * Like {@link #SPARK_CLIENT_VERSION_NATIVE} but also excludes all Spark / Scala runtime jars so + * the cluster's Spark distribution provides them (correct for {@code spark-submit}). + */ + public static final String SPARK_CLIENT_VERSION_NATIVE_PROVIDED = "native-provided"; + private static HopBeamGuiPlugin instance; /** @@ -121,7 +135,8 @@ public void menuToolsFatJar() { } try { - List jarFilenames = findInstalledJarFilenames(); + // Honour HOP_SPARK_CLIENT_VERSION (including special token "native") + List jarFilenames = findInstalledJarFilenames(resolveSparkClientVersion(null)); IRunnableWithProgress op = monitor -> { @@ -213,19 +228,23 @@ public void menuToolsExportMetadata() { *

Uses the default Spark client pack ({@code lib/spark-client}), or the versioned pack * selected by system property / env {@code HOP_SPARK_CLIENT_VERSION} under {@code * lib/spark-clients/}. Versioned packs under {@code lib/spark-clients/} are never all - * included at once. + * included at once. Special tokens {@link #SPARK_CLIENT_VERSION_NATIVE} and {@link + * #SPARK_CLIENT_VERSION_NATIVE_PROVIDED} build fat jars for the native Spark 4 engine. */ public static final List findInstalledJarFilenames() { return findInstalledJarFilenames(resolveSparkClientVersion(null)); } /** - * @param sparkClientVersion Spark client pack version (e.g. {@code 3.5.8}), or null/blank for the - * default pack at {@code lib/spark-client} + * @param sparkClientVersion Spark client pack version (e.g. {@code 3.5.8}), {@link + * #SPARK_CLIENT_VERSION_NATIVE}, {@link #SPARK_CLIENT_VERSION_NATIVE_PROVIDED}, or null/blank + * for the default pack at {@code lib/spark-client} */ public static final List findInstalledJarFilenames(String sparkClientVersion) { Set jarFiles = new HashSet<>(); - boolean versionedPack = StringUtils.isNotBlank(sparkClientVersion); + boolean nativeMode = isNativeSparkClientVersion(sparkClientVersion); + boolean nativeProvided = isNativeProvidedSparkClientVersion(sparkClientVersion); + boolean versionedPack = StringUtils.isNotBlank(sparkClientVersion) && !nativeMode; // lib/ tree except spark client pack directories (handled separately below) collectJarsExcludingSparkClientPacks(new File("lib"), jarFiles, versionedPack); @@ -247,42 +266,168 @@ public static final List findInstalledJarFilenames(String sparkClientVer jarFiles.addAll(FileUtils.listFiles(hopWebJars, new String[] {"jar"}, true)); } - // Selected Spark client pack only (never all versioned packs at once) - File sparkClientDir = resolveSparkClientPackDir(sparkClientVersion); - if (sparkClientDir.isDirectory()) { - jarFiles.addAll(FileUtils.listFiles(sparkClientDir, new String[] {"jar"}, false)); - } + if (nativeMode) { + // No Beam Spark 3 client pack; drop Scala 2.12 / Spark _2.12 / Beam Spark runner jars. + jarFiles.removeIf(HopBeamGuiPlugin::isExcludedForNativeSparkFatJar); + if (nativeProvided) { + // spark-submit: cluster provides Spark/Scala — do not embed plugin Spark 4 libs + jarFiles.removeIf(HopBeamGuiPlugin::isExcludedForNativeProvidedSparkFatJar); + } + } else { + // Selected Spark client pack only (never all versioned packs at once) + File sparkClientDir = resolveSparkClientPackDir(sparkClientVersion); + if (sparkClientDir.isDirectory()) { + jarFiles.addAll(FileUtils.listFiles(sparkClientDir, new String[] {"jar"}, false)); + } - // Final guard: when using a versioned pack, drop any spark-* jar outside that pack - // (avoids Beam fragments under lib/beam mixing serialVersionUID / version-info). - if (versionedPack) { - String packPath; - try { - packPath = sparkClientDir.getCanonicalPath(); - } catch (Exception e) { - packPath = sparkClientDir.getAbsolutePath(); + // Final guard: when using a versioned pack, drop any spark-* jar outside that pack + // (avoids Beam fragments under lib/beam mixing serialVersionUID / version-info). + // Note: this would also strip plugins/engines/spark Spark 4 jars — intentional for Beam + // packs; use spark-client-version=native for the native engine fat jar. + if (versionedPack) { + String packPath; + try { + packPath = sparkClientDir.getCanonicalPath(); + } catch (Exception e) { + packPath = sparkClientDir.getAbsolutePath(); + } + final String packPrefix = packPath; + jarFiles.removeIf( + f -> { + String n = f.getName(); + // Spark distribution jars are named spark-*.jar; Beam's runner is + // beam-runners-spark-* + if (!n.startsWith("spark-") || !n.endsWith(".jar")) { + return false; + } + try { + return !f.getCanonicalPath().startsWith(packPrefix); + } catch (Exception e) { + return !f.getAbsolutePath().startsWith(packPrefix); + } + }); } - final String packPrefix = packPath; - jarFiles.removeIf( - f -> { - String n = f.getName(); - // Spark distribution jars are named spark-*.jar; Beam's runner is beam-runners-spark-* - if (!n.startsWith("spark-") || !n.endsWith(".jar")) { - return false; - } - try { - return !f.getCanonicalPath().startsWith(packPrefix); - } catch (Exception e) { - return !f.getAbsolutePath().startsWith(packPrefix); - } - }); } List jarFilenames = new ArrayList<>(); jarFiles.forEach(file -> jarFilenames.add(file.toString())); + // Stable order improves FatJarBuilder "first wins" collision behaviour + jarFilenames.sort(String::compareTo); return jarFilenames; } + /** + * True when {@code version} is a native Spark fat-jar token ({@code native} or {@code + * native-provided}). + */ + public static boolean isNativeSparkClientVersion(String version) { + if (StringUtils.isBlank(version)) { + return false; + } + String v = version.trim(); + return SPARK_CLIENT_VERSION_NATIVE.equalsIgnoreCase(v) + || SPARK_CLIENT_VERSION_NATIVE_PROVIDED.equalsIgnoreCase(v); + } + + /** True when {@code version} is {@link #SPARK_CLIENT_VERSION_NATIVE_PROVIDED}. */ + public static boolean isNativeProvidedSparkClientVersion(String version) { + return StringUtils.isNotBlank(version) + && SPARK_CLIENT_VERSION_NATIVE_PROVIDED.equalsIgnoreCase(version.trim()); + } + + /** + * Whether a jar must be left out of a native Spark 4 fat jar (Beam Spark 3 / Scala 2.12 client + * noise). Public for unit tests. + */ + public static boolean isExcludedForNativeSparkFatJar(File jar) { + if (jar == null) { + return true; + } + String name = jar.getName(); + if (!name.endsWith(".jar")) { + return false; + } + + // Beam curated client packs (if any leaked into the set) + if (isUnderSparkClientPackDir(jar)) { + return true; + } + + // Beam Spark 3 runner (not used by the native engine) + if (name.startsWith("beam-runners-spark") && name.endsWith(".jar")) { + return true; + } + + // Spark 3 / Scala 2.12 artifacts (basename patterns) + if (name.startsWith("spark-") && name.contains("_2.12-")) { + return true; + } + if (name.startsWith("scala-library-2.12")) { + return true; + } + // chill_2.12-*, json4s-*_2.12-*, jackson-module-scala_2.12-*, scala-*_2.12-*, etc. + if (name.contains("_2.12-") && name.endsWith(".jar")) { + return true; + } + + return false; + } + + /** + * Extra exclusions for {@link #SPARK_CLIENT_VERSION_NATIVE_PROVIDED}: Spark/Scala runtime must + * come from the cluster (spark-submit), not the fat jar. + */ + public static boolean isExcludedForNativeProvidedSparkFatJar(File jar) { + if (jar == null) { + return true; + } + String name = jar.getName(); + if (!name.endsWith(".jar")) { + return false; + } + + // All Spark distribution modules + if (name.startsWith("spark-")) { + return true; + } + + // Scala runtime (any artifact naming convention used by Spark) + if (name.startsWith("scala-")) { + return true; + } + + // Common Spark-adjacent Scala libraries from plugins/engines/spark/lib + if (name.startsWith("chill_") + || name.startsWith("chill-java-") + || name.startsWith("json4s-") + || name.startsWith("jackson-module-scala_") + || name.startsWith("janino-") + || name.startsWith("commons-compiler-") + || name.startsWith("paranamer-")) { + return true; + } + + // Hadoop client pieces shipped next to Spark in the plugin lib (cluster provides these) + if (name.startsWith("hadoop-client-api-") || name.startsWith("hadoop-client-runtime-")) { + return true; + } + + return false; + } + + static boolean isUnderSparkClientPackDir(File jar) { + try { + String path = jar.getCanonicalPath().replace('\\', '/'); + return path.contains("/lib/spark-client/") + || path.contains("/lib/spark-clients/") + || path.endsWith("/lib/spark-client") + || path.matches(".*/lib/spark-clients/[^/]+/[^/]+\\.jar"); + } catch (Exception e) { + String path = jar.getAbsolutePath().replace('\\', '/'); + return path.contains("/lib/spark-client/") || path.contains("/lib/spark-clients/"); + } + } + /** * Resolve which Spark client pack version to use. Explicit argument wins, then system property * {@code HOP_SPARK_CLIENT_VERSION}, then env of the same name. @@ -302,9 +447,12 @@ public static String resolveSparkClientVersion(String explicitVersion) { return null; } - /** Directory for the default pack or {@code lib/spark-clients/}. */ + /** + * Directory for the default pack or {@code lib/spark-clients/}. Not used for {@link + * #SPARK_CLIENT_VERSION_NATIVE}. + */ public static File resolveSparkClientPackDir(String sparkClientVersion) { - if (StringUtils.isBlank(sparkClientVersion)) { + if (StringUtils.isBlank(sparkClientVersion) || isNativeSparkClientVersion(sparkClientVersion)) { return new File("lib/spark-client"); } return new File("lib/spark-clients/" + sparkClientVersion.trim()); diff --git a/plugins/engines/beam/src/test/java/org/apache/hop/beam/gui/HopBeamGuiPluginNativeFatJarTest.java b/plugins/engines/beam/src/test/java/org/apache/hop/beam/gui/HopBeamGuiPluginNativeFatJarTest.java new file mode 100644 index 00000000000..e05e0c8ef2f --- /dev/null +++ b/plugins/engines/beam/src/test/java/org/apache/hop/beam/gui/HopBeamGuiPluginNativeFatJarTest.java @@ -0,0 +1,128 @@ +/* + * 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.hop.beam.gui; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class HopBeamGuiPluginNativeFatJarTest { + + @TempDir File tempDir; + + @Test + void nativeTokenDetection() { + assertTrue(HopBeamGuiPlugin.isNativeSparkClientVersion("native")); + assertTrue(HopBeamGuiPlugin.isNativeSparkClientVersion("NATIVE")); + assertTrue(HopBeamGuiPlugin.isNativeSparkClientVersion(" native ")); + assertTrue(HopBeamGuiPlugin.isNativeSparkClientVersion("native-provided")); + assertTrue(HopBeamGuiPlugin.isNativeProvidedSparkClientVersion("native-provided")); + assertFalse(HopBeamGuiPlugin.isNativeProvidedSparkClientVersion("native")); + assertFalse(HopBeamGuiPlugin.isNativeSparkClientVersion(null)); + assertFalse(HopBeamGuiPlugin.isNativeSparkClientVersion("")); + assertFalse(HopBeamGuiPlugin.isNativeSparkClientVersion("3.5.8")); + } + + @Test + void providedModeExcludesSparkAndScalaRuntime() { + assertTrue( + HopBeamGuiPlugin.isExcludedForNativeProvidedSparkFatJar( + new File("spark-core_2.13-4.1.2.jar"))); + assertTrue( + HopBeamGuiPlugin.isExcludedForNativeProvidedSparkFatJar( + new File("spark-sql_2.13-4.1.2.jar"))); + assertTrue( + HopBeamGuiPlugin.isExcludedForNativeProvidedSparkFatJar( + new File("scala-library-2.13.17.jar"))); + assertTrue( + HopBeamGuiPlugin.isExcludedForNativeProvidedSparkFatJar(new File("chill_2.13-0.10.0.jar"))); + assertTrue( + HopBeamGuiPlugin.isExcludedForNativeProvidedSparkFatJar( + new File("hadoop-client-api-3.4.2.jar"))); + assertFalse( + HopBeamGuiPlugin.isExcludedForNativeProvidedSparkFatJar( + new File("hop-engines-spark-2.19.0-SNAPSHOT.jar"))); + assertFalse( + HopBeamGuiPlugin.isExcludedForNativeProvidedSparkFatJar( + new File("beam-runners-direct-java-2.74.0.jar"))); + // Lakehouse connectors ship in engines-spark lib and must ride the native-provided fat jar + // so cluster submit does not require a separate --packages step for Delta/Iceberg. + assertFalse( + HopBeamGuiPlugin.isExcludedForNativeProvidedSparkFatJar( + new File("delta-spark_4.1_2.13-4.3.1.jar"))); + assertFalse( + HopBeamGuiPlugin.isExcludedForNativeProvidedSparkFatJar( + new File("delta-storage-4.3.1.jar"))); + assertFalse( + HopBeamGuiPlugin.isExcludedForNativeProvidedSparkFatJar( + new File("iceberg-spark-runtime-4.1_2.13-1.11.0.jar"))); + assertFalse( + HopBeamGuiPlugin.isExcludedForNativeProvidedSparkFatJar( + new File("unitycatalog-client-0.5.0.jar"))); + } + + @Test + void excludesBeamSpark3AndScala212Basenames() { + assertTrue(excluded("spark-core_2.12-3.5.8.jar")); + assertTrue(excluded("spark-sql-api_2.12-3.5.0.jar")); + assertTrue(excluded("scala-library-2.12.21.jar")); + assertTrue(excluded("chill_2.12-0.10.0.jar")); + assertTrue(excluded("json4s-core_2.12-3.7.0-M11.jar")); + assertTrue(excluded("jackson-module-scala_2.12-2.21.0.jar")); + assertTrue(excluded("beam-runners-spark-3-2.74.0.jar")); + } + + @Test + void keepsNativeSpark4AndUnrelatedJars() { + assertFalse(excluded("spark-core_2.13-4.1.2.jar")); + assertFalse(excluded("spark-sql_2.13-4.1.2.jar")); + assertFalse(excluded("scala-library-2.13.17.jar")); + assertFalse(excluded("hop-engines-spark-2.19.0-SNAPSHOT.jar")); + assertFalse(excluded("beam-runners-direct-java-2.74.0.jar")); + assertFalse(excluded("beam-runners-flink-1.19-2.74.0.jar")); + assertFalse(excluded("hadoop-client-api-3.4.2.jar")); + } + + @Test + void excludesJarsUnderSparkClientPackPaths() throws Exception { + File packJar = new File(tempDir, "lib/spark-client/spark-core_2.12-3.5.8.jar"); + assertTrue(packJar.getParentFile().mkdirs()); + assertTrue(packJar.createNewFile()); + assertTrue(HopBeamGuiPlugin.isExcludedForNativeSparkFatJar(packJar)); + + File versioned = new File(tempDir, "lib/spark-clients/3.4.4/spark-core_2.12-3.4.4.jar"); + assertTrue(versioned.getParentFile().mkdirs()); + assertTrue(versioned.createNewFile()); + assertTrue(HopBeamGuiPlugin.isExcludedForNativeSparkFatJar(versioned)); + } + + @Test + void keepsSpark4UnderNativePluginLibPath() throws Exception { + File pluginJar = new File(tempDir, "plugins/engines/spark/lib/spark-sql_2.13-4.1.2.jar"); + assertTrue(pluginJar.getParentFile().mkdirs()); + assertTrue(pluginJar.createNewFile()); + assertFalse(HopBeamGuiPlugin.isExcludedForNativeSparkFatJar(pluginJar)); + } + + private static boolean excluded(String basename) { + return HopBeamGuiPlugin.isExcludedForNativeSparkFatJar(new File(basename)); + } +} diff --git a/plugins/engines/pom.xml b/plugins/engines/pom.xml index e03f28ca507..76641fd9fbc 100644 --- a/plugins/engines/pom.xml +++ b/plugins/engines/pom.xml @@ -32,6 +32,7 @@ beam single-threaded + spark diff --git a/plugins/engines/spark/README.md b/plugins/engines/spark/README.md new file mode 100644 index 00000000000..25b455d5c76 --- /dev/null +++ b/plugins/engines/spark/README.md @@ -0,0 +1,455 @@ + + +# Native Apache Spark pipeline engine + +Implements [issue #7486](https://github.com/apache/hop/issues/7486): a Hop +`IPipelineEngine` that runs batch pipelines on **Apache Spark 4.1.x** without +Apache Beam. + +## Design notes (Phase 0 decisions) + +| Topic | Choice | +|-------|--------| +| Spark version | `4.1.2` (`spark-sql_2.13`), independent of Beam’s Spark 3.x | +| Row model | Spark `Row` + `StructType` (`HopSparkRowConverter`) | +| Hop transforms | `mapPartitions` via `HopMapPartitionsFn` (init once per partition) | +| Metadata on the wire | Transform XML + `JsonRowMeta` + `SerializableMetadataProvider` JSON | +| Deployment v1 | `local[*]` and standalone `spark://`; fat-jar field for cluster | +| Streaming / windows | Out of scope — use Beam Spark | +| Stateful transforms | Native handlers: Memory Group By, Merge Join, Unique Rows, Sort Rows; sorted Group By remains banned | + +## Classpath + +- **Compile / local run:** Spark 4.1 is packaged under `plugins/engines/spark/lib` + so Hop can start `local[*]` without a separate Spark install. The plugin must + also ship **hadoop-client-api/runtime** and **log4j**: Spark 4 loads + `org.apache.spark.sql.classic.SparkSession` reflectively and fails with + *“Cannot find a SparkSession implementation on the Classpath”* when those + classes are missing (the real `ClassNotFoundException` is swallowed). +- **Fat jar tokens (not real pack directories under `lib/spark-clients/`):** + + | Token | Use for | Spark runtime | + |-------|---------|----------------| + | `native` | hop-run client / embed Spark | Ships Spark 4 from `plugins/engines/spark/lib` | + | **`native-provided`** | **`spark-submit` on a cluster** | **Cluster provides Spark** (do not embed) | + + **Do not** embed Spark inside the app jar for `spark-submit`. That mixes two + Spark copies on the driver classpath and fails on Java 21 (e.g. + `DirectByteBuffer` / wrong `Running Spark version`). + + ```bash + # From a Hop install — fat jar for spark-submit: + ./hop-conf.sh --generate-fat-jar=/tmp/hop-native-spark4-submit.jar \ + --spark-client-version=native-provided + ``` + +- **spark-submit (cluster):** Use driver main class + `org.apache.hop.spark.run.MainSpark` with a **native-provided** fat jar: + + ```bash + # Critical: pin SPARK_HOME to this distro. If SPARK_HOME points at an older + # install (e.g. /opt/spark -> 3.3.0), even "./bin/spark-submit" from a 4.1.x + # tree re-executes ${SPARK_HOME}/bin/spark-class and loads the wrong Spark. + export SPARK_HOME=~/Downloads/spark-4.1.3-bin-hadoop3 + + # Export project metadata to JSON, then: + "${SPARK_HOME}/bin/spark-submit" \ + --master spark://spark:7077 \ + --class org.apache.hop.spark.run.MainSpark \ + /tmp/hop-native-spark4-submit.jar \ + /path/to/pipeline.hpl \ + /path/to/metadata.json \ + YourNativeSparkRunConfig + ``` + + Positional args: `pipeline.hpl`, `metadata.json`, `runConfigName`, optional + env config file. Named form also works: `--HopPipelinePath=...`, + `--HopMetadataPath=...`, `--HopRunConfigurationName=...`, + `--HopConfigFile=...`. + + The run configuration must use the **native** Spark pipeline engine (not + Beam). Prefer leaving **master blank** so `spark-submit --master` wins, or set + master to `spark://spark:7077`. Leave **fatJar empty** under submit. + Paths must be readable on the driver; data paths must be reachable from workers. +- After rebuilding the plugin, reinstall into your Hop distro (or rebuild + `assemblies/client`) so `plugins/engines/spark/lib` is updated. + +## Execution information location + +Attach an **execution information location** (and optionally an execution data +profile) on the native Spark **pipeline run configuration**, same as Local/Beam. + +The driver will: + +1. Load and initialize the location at prepare time +2. `registerExecution` when the pipeline starts +3. Periodically `updateExecutionState` for the pipeline and each transform + component (metrics come from Spark accumulators / `EngineMetrics`) +4. Write a final state and `close` the location when the pipeline finishes, + fails, or is stopped + +Notes: + +- **State updates** (`updateExecutionState`) run on the **driver** from + `EngineMetrics` (all transform types). +- **Row sampling** (`registerData`) runs on **executors** for generic + mapPartitions transforms when the run configuration also names an + **execution data profile** (First/Last/Random rows, etc.). Extra GUI + samplers on the engine are serialized into the closure like Beam. +- File locations used for sampling must be **writable from every executor** + (shared FS / object store). Local-only paths only receive driver state. +- For `spark-submit`, export location + data profile into the metadata JSON. + +## Module layout + +- `engines/` — `SparkPipelineEngine`, run configuration, capabilities +- `pipeline/` — Hop → Spark converter and transform handlers +- `pipeline/handler/` — native shuffle handlers + generic mapPartitions +- `core/` — `HopMapPartitionsFn`, row conversion, executor-side Hop init +- `run/` — `MainSpark` for spark-submit + +## Native handlers (Phase 3) + +| Plugin id | Spark API | Notes | +|-----------|-----------|--------| +| `MemoryGroupBy` | `groupBy` + agg | SUM, AVG, MIN, MAX, COUNTs, FIRST/LAST, STDDEV, percentile, concat variants | +| `MergeJoin` | `Dataset.join` | INNER / LEFT / RIGHT / FULL OUTER | +| `Unique` | `dropDuplicates` (+ count via groupBy) | Reject-to-error not supported | +| `SortRows` | `orderBy` | Optional unique-on-keys | +| `GroupBy` (sorted) | — | **Banned** — use Memory Group By | +| `SparkFileInput` | `spark.read.format(...).load` | CSV/Parquet/JSON/ORC/text | +| `SparkFileOutput` | `df.write.format(...).save` | Save modes, optional coalesce/partitionBy | + +## Classic Hop I/O (POC) + +`HopMapPartitionsFn` sets partition-scoped variables for classic writers: + +| Variable | Value | +|----------|--------| +| `${Internal.Transform.ID}` | `{transformName}-{partitionId}` | +| `${Internal.Transform.CopyNr}` / `BundleNr` | partition id | + +It also enables `beamContext` so Text File Output auto-appends `_id_bundle` like Beam. +`dependencies.xml` stages `textfile`, `getfilenames`, and `excel` for mapPartitions POCs +(Get File Names → Text File Input, small Excel lookups, etc.). Pure sources still run on +one partition so files are not listed/read N times. + +## File I/O transforms + +Dedicated **Spark File Input** / **Spark File Output** transforms (not Hop Text File Input) +provide a clean Dataset API mapping: + +- **Input:** path, format, header/separator/quote (CSV), optional field schema or `inferSchema` +- **Output:** path, format, save mode (Overwrite/Append/Ignore/ErrorIfExists), header, coalesce, + partition-by columns + +**Paths are Spark/Hadoop URIs** (e.g. local path, `hdfs://…`, `s3a://…`), not Hop VFS schemes +(`s3://`, named MinIO `minio://`, …). Dataset `load`/`save` never call `HopVfs`. Classic +mapPartitions transforms still use Hop VFS. See user manual: *Paths and file systems on native +Spark*. + +Optional **path scheme map** on the run configuration (`from=to` lines, e.g. `s3=s3a`) rewrites +URI schemes for Spark File / Lake PATH only via `SparkPathDialect.toSparkUri`. `SparkPathDialect` +also appends a hint when a known Hop-only scheme still appears in I/O errors after mapping. + +## Project package (nested Simple Mapping / Pipeline Executor) + +**Specifically for Native Spark execution** (not File → Export current project to zip). + +```bash +# GUI: Tools → Export project package for Native Spark… +# CLI: +./hop-conf.sh -j my-project --export-spark-project=/tmp/my-project-spark.zip + +# spark-submit +--class org.apache.hop.spark.run.MainSpark ... \ + --HopProjectPackage=/tmp/my-project-spark.zip \ + --HopPipelinePath=pipelines/run.hpl \ + --HopRunConfigurationName=YourNativeSparkRunConfig +``` + +`SparkProjectPackage` + `SparkPipelineEngine`: + +1. Driver stages the zip and sets `HOP_SPARK_PROJECT_PACKAGE` +2. Session start: `SparkContext.addFile` ships the zip to **all executors** (`SparkFiles`) +3. Mini-pipeline init: `SparkFiles.get` → extract under `java.io.tmpdir` → `PROJECT_HOME` + +Do **not** use package `PROJECT_HOME` as the root for Spark Dataset data paths. + +## Cluster walk-through (Docker) + +Sample project: `src/samples/spark-demo` +Compose: `docker/integration-tests/integration-tests-spark-native-cluster.yaml` (Spark **4.1.2**) + +```bash +./plugins/engines/spark/src/samples/spark-demo/scripts/prepare-dist.sh +# artifacts default to /tmp/spark-demo-dist (override DIST_DIR / HOP_DIST_DIR) +# shared data plane: /tmp/spark-demo-dist/hop-data → /data/hop-data (host-visible) +docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml \ + up -d --build --scale spark-worker=2 +docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml \ + exec spark /opt/hop-samples/spark-demo/scripts/spark-submit-demo.sh +# Workflow Executor demo: +docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml \ + exec spark /opt/hop-samples/spark-demo/scripts/spark-submit-workflows-demo.sh +# Host inspect: ls /tmp/spark-demo-dist/hop-data/out /tmp/spark-demo-dist/hop-data/executions +``` + +See user manual: *Cluster demo walk-through*. + +Writes run as Spark actions during graph build; the output handler registers an empty leaf +Dataset so a later engine `count()` does not re-write files. + +## Lakehouse connectors (Delta Lake / Apache Iceberg) + +**Open table format** support on the native Spark engine (ACID tables, catalogs, time travel, +MERGE, maintenance). Connectors **ship in the default** `hop-engines-spark` plugin assembly: + +| Path | Contents | +|------|----------| +| `plugins/engines/spark/lib/delta/` | Delta Lake 4.3.1 tree | +| `plugins/engines/spark/lib/iceberg/` | Iceberg Spark runtime 1.11.0 | + +**User documentation (Antora):** +`docs/hop-user-manual/.../pipeline/spark/lakehouse.adoc` plus transform pages +`spark-lake-table-*.adoc` and `metadata-types/spark-catalog.adoc`. +**Integration tests:** `integration-tests/lakehouse` (Delta) and +`integration-tests/iceberg` (Iceberg) — PATH Input/Output, overwrite/time travel, MERGE. + +| Pin | Coordinate | +|-----|------------| +| Spark | `spark-sql_2.13:4.1.2` (existing) | +| Delta Lake | `io.delta:delta-spark_4.1_2.13:4.3.1` | +| Apache Iceberg | `org.apache.iceberg:iceberg-spark-runtime-4.1_2.13:1.11.0` | + +The `_4.1_2.13` suffix means **Spark 4.1 / Scala 2.13**, not the product major version. + +### Classloader (why not a sibling plugin) + +`SparkPipelineEngine` sets TCCL to the **Spark engine plugin classloader**, which loads: + +1. JARs under `plugins/engines/spark/lib/` (recursive — includes `lib/delta`, `lib/iceberg`) +2. Folders from `plugins/engines/spark/dependencies.xml` +3. The engine plugin jar + +A sibling plugin under `plugins/engines/spark-delta` is a **separate** classloader and is +**not** visible to `SparkSession` / DataSource resolution. Connectors must live **inside** +the engine lib tree (as shipped) or on the spark-submit driver/executor classpath. + +### hop-run / GUI `local[*]` + +No manual overlay is required on a normal Hop install. After rebuilding the plugin: + +```bash +./mvnw -pl plugins/engines/spark -am package +# zip / client assembly unpacks lib/delta and lib/iceberg under plugins/engines/spark/ +``` + +`target/lakehouse-connectors/` is a frozen jar list for docs verification (generated every build). + +### spark-submit + +Use a **native-provided** fat jar so Spark is not embedded twice. Lakehouse jars under +`plugins/engines/spark/lib/{delta,iceberg}` are **included** in that fat jar (they are not +filtered out like Spark/Scala/Hadoop client). Optional `--packages` only if you strip them: + +```bash +export SPARK_HOME=/path/to/spark-4.1.x-bin-hadoop3 + +"${SPARK_HOME}/bin/spark-submit" \ + --master spark://spark:7077 \ + --conf spark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension,org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions \ + --conf spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog \ + --class org.apache.hop.spark.run.MainSpark \ + /tmp/hop-native-provided.jar \ + /path/to/pipeline.hpl \ + /path/to/metadata.json \ + YourNativeSparkRunConfig +``` + +When both Delta and Iceberg are used, keep **Delta** on `spark_catalog` and register Iceberg +catalogs under **other** names (e.g. `lake`). + +### Session conf matrix (spike / future transforms) + +| When | Conf | +|------|------| +| Any Delta use | `spark.sql.extensions` includes `io.delta.sql.DeltaSparkSessionExtension` | +| Any Delta use | `spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog` | +| Any Iceberg use | `spark.sql.extensions` includes `org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions` | +| Iceberg TABLE | `spark.sql.catalog.=…` (+ type/warehouse/uri) | + +### Spike findings (PR 1 — 2026-07, Spark 4.1.2 + Delta 4.3.1 + Iceberg 1.11.0) + +Recorded by `SparkLakehouseConnectorTest` with `-Plakehouse`: + +| Check | Result | +|-------|--------| +| `Class.forName(DeltaSparkSessionExtension)` with connector on CL | OK | +| PATH write/read with extension **and** DeltaCatalog | OK (required gate) | +| PATH write/read with extension **only** (no DeltaCatalog) | **Fails** with `DELTA_CONFIGURE_SPARK_SESSION_WITH_EXTENSION_AND_CATALOG` | +| Iceberg extension on CL | OK | +| Iceberg bare `format("iceberg").save/load(path)` (no catalog conf) | **Fails** — defaults toward `HiveCatalog` / needs HMS jars | +| Iceberg `writeTo(hop_iceberg.\`file:///path\`)` + SQL read (PR 3) | **OK** | +| Iceberg Hadoop warehouse + `writeTo("local.db.t")` + `spark.table` | OK (named-table style) | +| Iceberg TABLE `writeTo` vs `saveAsTable` | **writeTo for PATH**; formal TABLE lock still PR 5 / KD-24 | + +**Tiered probe default (updated by spike + PR 3):** + +- Always require connector classes on the classpath. +- **Any Delta use (including PATH-only)** requires session-registered + `DeltaSparkSessionExtension` **and** `spark.sql.catalog.spark_catalog=…DeltaCatalog`. +- **Any Iceberg PATH use** requires Iceberg extensions + Hadoop catalog `hop_iceberg` + (auto-registered on hop-run; can be set at runtime on active sessions). +- hop-run always applies full Delta/Iceberg conf when those formats are present. + +### Frozen jar list (shipped under `lib/delta` + `lib/iceberg`) + +Also staged into `target/lakehouse-connectors/` on every build for docs verification. + +| Jar | Role | +|-----|------| +| `delta-spark_4.1_2.13-4.3.1.jar` | Delta Spark SQL / DataSource | +| `delta-storage-4.3.1.jar` | Delta storage layer | +| `delta-kernel-api-4.3.1.jar` | Delta Kernel API | +| `delta-kernel-defaults-4.3.1.jar` | Delta Kernel defaults | +| `delta-kernel-unitycatalog-4.3.1.jar` | Transitive from delta-spark 4.3.1 | +| `unitycatalog-client-0.5.0.jar` | Transitive (UC-related) | +| `unitycatalog-hadoop-0.5.0.jar` | Transitive (UC-related) | +| `iceberg-spark-runtime-4.1_2.13-1.11.0.jar` | Shaded Iceberg Spark runtime | + +### Probe API + +`org.apache.hop.spark.table.SparkLakeConnectorProbe` verifies connector classes are loadable +and throws `HopException` with reinstall / `--packages` guidance when missing. +`LakeSessionPlan` scans active lake transforms before `SparkSession` build: probes the +classpath and applies Delta extension + `DeltaCatalog` on new hop-run sessions. + +### Lake Table transforms (PR 2–3 — Delta + Iceberg PATH) + +| Plugin id | Role | +|-----------|------| +| `SparkLakeTableInput` | Read Delta or Iceberg PATH tables | +| `SparkLakeTableOutput` | Write Delta or Iceberg PATH; **default save mode ErrorIfExists** | + +| Format | PATH read | PATH write | +|--------|-----------|------------| +| **Delta** | `spark.read.format("delta").load(path)` | `format("delta").mode(…).save(path)` | +| **Iceberg** | SQL `SELECT * FROM hop_iceberg.\`file:///…\`` | `writeTo(hop_iceberg.\`uri\`).using("iceberg")` | + +Iceberg PATH uses a built-in Hadoop catalog named **`hop_iceberg`** (not `spark_catalog`, so it +can co-exist with Delta’s `DeltaCatalog`). Bare `format("iceberg").load(path)` is **not** used — +on Spark 4.1 + Iceberg 1.11 it defaults toward HiveCatalog and fails without HMS jars. + +hop-run auto-registers: + +| When | Session conf | +|------|----------------| +| Any Delta | extensions + `spark.sql.catalog.spark_catalog=…DeltaCatalog` | +| Any Iceberg | extensions + `spark.sql.catalog.hop_iceberg=…SparkCatalog` (type=hadoop, warehouse under tmp) | + +Classic **Spark File Input/Output** are unchanged (csv/parquet/json/orc/text only). + +### Time travel (PR 4) + +Lake Table Input supports: + +| UI type | Delta option | Iceberg SQL | +|---------|--------------|-------------| +| `NONE` | (current) | (current) | +| `VERSION` | `versionAsOf` | `VERSION AS OF ` | +| `TIMESTAMP` | `timestampAsOf` | `TIMESTAMP AS OF TIMESTAMP '…'` | + +Example: write twice with Overwrite, then set Time travel = VERSION and Version = `0` (Delta) +or the first snapshot id from `{table}.snapshots` (Iceberg). + +### Spark Catalog metadata + TABLE mode (PR 5) + +**Metadata type:** `SparkCatalog` (category Connections) — Spark SQL catalog conf for Iceberg +Hadoop/REST (or custom). Fields: catalog name, type, warehouse/uri, optional credential, extra +`key=value` lines → `spark.sql.catalog..*`. + +Lake Table transforms in **TABLE** mode: + +| Field | Purpose | +|-------|---------| +| Identifier mode | `TABLE` | +| Table identifier | e.g. `lake.db.orders` | +| Spark Catalog metadata name | Hop metadata entry to register (optional if catalog already on session) | + +| Format | TABLE read | TABLE write | +|--------|------------|-------------| +| **Iceberg** (primary) | `SELECT * FROM catalog.ns.table` (+ time travel clauses) | `writeTo(id).using("iceberg")` | +| **Delta** (advanced) | `format("delta").table(id)` | `format("delta").saveAsTable(id)` | + +`LakeSessionPlan` loads referenced `SparkCatalog` entries and applies them when building the +session (hop-run) or registers them on an active session when possible (submit). + +For spark-submit, export project metadata JSON including `SparkCatalog` definitions, or pass +equivalent `--conf spark.sql.catalog.=…`. + +### MERGE (PR 6) + +**Spark Lake Table Merge** — single upstream hop supplies source rows; target is PATH or TABLE. + +| Field | Notes | +|-------|--------| +| Merge condition | ON clause, aliases `t` (target) and `s` (source) | +| When matched | `UPDATE_ALL` / `DELETE` / `NONE` | +| When not matched | `INSERT_ALL` / `NONE` | +| Not matched by source | optional `DELETE` (Delta; Iceberg depends on version) | +| Raw MERGE SQL | advanced override (empty default) | + +Target SQL ids: Delta PATH → `delta.\`path\``; Iceberg PATH → `hop_iceberg.\`uri\``; TABLE → multi-part id. + +Metrics are log-only (merge completion), not Dataset row counts. + +### Maintenance (PR 7) + +**Spark Lake Table Maintenance** — zero-input action (no hop required). Destructive ops require +`acknowledgeDestructive`. + +| Operation | Delta | Iceberg | +|-----------|-------|---------| +| OPTIMIZE | `OPTIMIZE delta.\`path\` [ZORDER BY …]` | `CALL cat.system.rewrite_data_files` | +| VACUUM | `VACUUM … RETAIN n HOURS` (**n required**) | n/a → use EXPIRE_SNAPSHOTS | +| EXPIRE_SNAPSHOTS | n/a | `CALL … expire_snapshots(retain_last => N)` | +| REWRITE_MANIFESTS | n/a | `CALL … rewrite_manifests` | +| DELETE_WHERE | `DELETE FROM … WHERE …` (**WHERE required**) | same | + +Metrics are log-only (expect 0 in/out in the GUI). + + +### RAT / license + +Delta Lake and Apache Iceberg are Apache-2.0 licensed and redistributed under +`plugins/engines/spark/lib/{delta,iceberg}/`. Re-check transitive licenses when pins change. +Unity Catalog client jars ship as Delta transitives (Apache-2.0). + +## Build + +```bash +# Unit + lakehouse ITs (connectors are default runtime deps) +./mvnw -pl plugins/engines/spark -am test + +# Package plugin zip (includes lib/delta + lib/iceberg) +./mvnw -pl plugins/engines/spark -am package + +# -Plakehouse is a no-op alias retained for older scripts +./mvnw -pl plugins/engines/spark -am test -Plakehouse +``` diff --git a/plugins/engines/spark/pom.xml b/plugins/engines/spark/pom.xml new file mode 100644 index 00000000000..03fe995bf94 --- /dev/null +++ b/plugins/engines/spark/pom.xml @@ -0,0 +1,269 @@ + + + + 4.0.0 + + + org.apache.hop + hop-plugins-engines + 2.19.0-SNAPSHOT + + + hop-engines-spark + jar + Hop Plugins Engines Spark + Native Apache Spark 4.x pipeline engine for Hop (issue #7486) + + + + 4.3.1 + + 3.4.2 + + jackson-core + jackson-databind + jackson-annotations + 1.11.0 + + ${project.basedir}/target/lakehouse-connectors + 2.13 + + 4.1.2 + + 4.1 + + + + + + jakarta.servlet + jakarta.servlet-api + 5.0.0 + + + + org.apache.hadoop + hadoop-client-api + ${hadoop.client.version} + + + org.apache.hadoop + hadoop-client-runtime + ${hadoop.client.version} + + + + + org.apache.spark + spark-sql_${scala.binary.version} + ${spark.version} + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + org.slf4j + * + + + + + org.apache.hop + hop-core + ${project.version} + provided + + + org.apache.hop + hop-engine + ${project.version} + provided + + + + + org.apache.hop + hop-transform-memgroupby + ${project.version} + provided + + + org.apache.hop + hop-transform-mergejoin + ${project.version} + provided + + + org.apache.hop + hop-transform-sort + ${project.version} + provided + + + org.apache.hop + hop-transform-uniquerows + ${project.version} + provided + + + org.apache.hop + hop-ui + ${project.version} + provided + + + + + io.delta + delta-spark_${spark.version.prefix}_${scala.binary.version} + ${delta.version} + runtime + + + com.fasterxml.jackson.core + * + + + org.apache.spark + * + + + org.scala-lang + * + + + org.slf4j + * + + + + + org.apache.iceberg + iceberg-spark-runtime-4.1_${scala.binary.version} + ${iceberg.version} + runtime + + + com.fasterxml.jackson.core + * + + + org.apache.spark + * + + + org.scala-lang + * + + + org.slf4j + * + + + + + + org.apache.hop + hop-transform-filterrows + ${project.version} + test + + + + org.apache.hop + hop-transform-groupby + ${project.version} + test + + + + org.apache.hop + hop-transform-streamlookup + ${project.version} + test + + + + org.apache.hop + hop-transform-textfile + ${project.version} + test + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-lakehouse-connectors + + copy-dependencies + + generate-resources + + ${lakehouse.connectors.dir} + delta-spark_4.1_2.13,delta-storage,delta-kernel-api,delta-kernel-defaults,delta-kernel-unitycatalog,iceberg-spark-runtime-4.1_2.13,unitycatalog-client,unitycatalog-hadoop,commons-pool + false + false + true + + + + + + + + + + + lakehouse + + + diff --git a/plugins/engines/spark/src/assembly/assembly.xml b/plugins/engines/spark/src/assembly/assembly.xml new file mode 100644 index 00000000000..2fdfbd477ed --- /dev/null +++ b/plugins/engines/spark/src/assembly/assembly.xml @@ -0,0 +1,90 @@ + + + + hop-engines-spark + + zip + + . + + + ${project.basedir}/src/main/resources/version.xml + plugins/engines/spark + true + + + ${project.basedir}/src/main/resources/dependencies.xml + plugins/engines/spark + true + + + + + + org.apache.hop:hop-engines-spark:jar + + plugins/engines/spark + + + + runtime + + org.apache.hop:hop-engines-spark:jar + org.apache.hop:*:jar + org.slf4j:*:jar + com.fasterxml.jackson.core:jackson-core:jar + com.fasterxml.jackson.core:jackson-databind:jar + com.fasterxml.jackson.core:jackson-annotations:jar + io.delta:* + org.apache.iceberg:* + io.unitycatalog:* + + plugins/engines/spark/lib + + + + + + ${project.build.directory}/lakehouse-connectors + plugins/engines/spark/lib/delta + + delta-*.jar + unitycatalog-*.jar + commons-pool*.jar + + + + ${project.build.directory}/lakehouse-connectors + plugins/engines/spark/lib/iceberg + + iceberg-*.jar + + + + diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/config/ExportSparkProjectConfigPlugin.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/config/ExportSparkProjectConfigPlugin.java new file mode 100644 index 00000000000..bc18b0b4ee5 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/config/ExportSparkProjectConfigPlugin.java @@ -0,0 +1,188 @@ +/* + * 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.hop.spark.config; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.vfs2.FileObject; +import org.apache.commons.vfs2.FileType; +import org.apache.hop.core.Const; +import org.apache.hop.core.config.plugin.ConfigPlugin; +import org.apache.hop.core.config.plugin.IConfigOptions; +import org.apache.hop.core.encryption.Encr; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.core.vfs.HopVfs; +import org.apache.hop.metadata.api.IHasHopMetadataProvider; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.metadata.serializer.json.JsonMetadataProvider; +import org.apache.hop.spark.pkg.PackageExportFilter; +import org.apache.hop.spark.pkg.SparkProjectPackage; +import picocli.CommandLine; + +/** + * hop-conf option to build a Native Spark project package zip from a project home folder and its + * {@code metadata/} directory. + * + *

Preferred usage (no Hop project registration required): + * + *

+ * hop-conf.sh --export-spark-project=/tmp/demo.zip \
+ *   --export-spark-project-home=/path/to/spark-demo
+ * 
+ * + *

Alternatively enable a registered project first with {@code -j} (not {@code + * --project} / {@code -p}, which only name a project for create/delete/list management): + * + *

+ * hop-conf.sh -j my-project --export-spark-project=/tmp/demo.zip
+ * 
+ */ +@ConfigPlugin( + id = "ExportSparkProjectConfigPlugin", + description = "Export a Native Spark project package zip (definitions + metadata.json)") +public class ExportSparkProjectConfigPlugin implements IConfigOptions { + + @CommandLine.Option( + names = {"-esp", "--export-spark-project"}, + description = + "Export a Native Spark project package zip (project/ tree + root metadata.json). " + + "Skip bulk datasets/. Pair with --export-spark-project-home= or enable a " + + "project with -j first (not --project/-p). MainSpark: --HopProjectPackage=.") + private String exportSparkProjectZip; + + @CommandLine.Option( + names = {"--export-spark-project-home"}, + description = + "Project home folder to package. Defaults to PROJECT_HOME when a project was enabled " + + "with -j . Prefer this for scripts so you need not register the project.") + private String exportSparkProjectHome; + + @CommandLine.Option( + names = {"--export-spark-project-exclude-dirs"}, + description = + "Comma-separated extra directory basenames to skip (merged with defaults and " + + "spark-package.json). Example: tmp,screenshots") + private String exportExcludeDirs; + + @CommandLine.Option( + names = {"--export-spark-project-exclude-globs"}, + description = + "Comma-separated exclude globs relative to project home (e.g. **/*.jar,**/*.zip)") + private String exportExcludeGlobs; + + @CommandLine.Option( + names = {"--export-spark-project-include"}, + description = + "Comma-separated relative paths to force-include even under skipped dirs " + + "(e.g. work/cluster-env.json)") + private String exportIncludePaths; + + @CommandLine.Option( + names = {"--export-spark-project-replace-default-excludes"}, + description = + "If set, do not apply built-in exclude dirs (work, datasets, target, …); use only " + + "spark-package.json and --export-spark-project-exclude-dirs", + defaultValue = "false") + private boolean exportReplaceDefaultExcludes; + + @Override + public boolean handleOption( + ILogChannel log, IHasHopMetadataProvider hasHopMetadataProvider, IVariables variables) + throws HopException { + if (StringUtils.isEmpty(exportSparkProjectZip)) { + return false; + } + + String projectHome = resolveProjectHome(variables); + String zip = variables.resolve(exportSparkProjectZip); + + log.logBasic("Exporting Native Spark project package from home=" + projectHome); + log.logBasic("Destination zip: " + zip); + + IHopMetadataProvider metadataProvider = + resolveMetadataProvider(projectHome, hasHopMetadataProvider, variables, log); + + PackageExportFilter cliFilter = + new PackageExportFilter( + PackageExportFilter.splitCsv(exportExcludeDirs), + PackageExportFilter.splitCsv(exportExcludeGlobs), + PackageExportFilter.splitCsv(exportIncludePaths), + exportReplaceDefaultExcludes); + SparkProjectPackage.exportProject(projectHome, zip, metadataProvider, variables, cliFilter); + log.logBasic( + "Spark project package written. Submit with MainSpark " + + "--HopProjectPackage=" + + zip + + " --HopPipelinePath= --HopRunConfigurationName="); + return true; + } + + private String resolveProjectHome(IVariables variables) throws HopException { + if (StringUtils.isNotEmpty(exportSparkProjectHome)) { + return variables.resolve(exportSparkProjectHome); + } + String projectHome = variables.getVariable("PROJECT_HOME"); + if (StringUtils.isNotEmpty(projectHome)) { + return variables.resolve(projectHome); + } + throw new HopException( + "No project home for export. Either pass " + + "--export-spark-project-home=/path/to/project " + + "or enable a registered project first with -j " + + "(short option -j). Note: --project / -p only names a project for " + + "create/delete/list and does not set PROJECT_HOME."); + } + + /** + * Prefer {@code /metadata} on disk so export works without enabling a Hop project. + * Fall back to the hop-conf metadata provider (used when -j already reconfigured it). + */ + private IHopMetadataProvider resolveMetadataProvider( + String projectHome, + IHasHopMetadataProvider hasHopMetadataProvider, + IVariables variables, + ILogChannel log) + throws HopException { + try { + String metaFolder = projectHome; + if (!metaFolder.endsWith("/") && !metaFolder.endsWith("\\")) { + metaFolder = metaFolder + Const.FILE_SEPARATOR; + } + metaFolder = metaFolder + "metadata"; + FileObject metaFo = HopVfs.getFileObject(metaFolder); + if (metaFo.exists() && metaFo.getType() == FileType.FOLDER) { + log.logBasic("Loading metadata from " + metaFolder); + return new JsonMetadataProvider(Encr.getEncoder(), metaFolder, variables); + } + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException("Unable to open project metadata folder under " + projectHome, e); + } + + if (hasHopMetadataProvider != null && hasHopMetadataProvider.getMetadataProvider() != null) { + log.logBasic("Using hop-conf metadata provider (no metadata/ folder under project home)"); + return hasHopMetadataProvider.getMetadataProvider(); + } + throw new HopException( + "No metadata found under " + + projectHome + + "/metadata and no hop-conf metadata provider is available"); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/HopMapPartitionsFn.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/HopMapPartitionsFn.java new file mode 100644 index 00000000000..e310fd2034b --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/HopMapPartitionsFn.java @@ -0,0 +1,1110 @@ +/* + * 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.hop.spark.core; + +import java.io.File; +import java.io.Serializable; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.Const; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.exception.HopRuntimeException; +import org.apache.hop.core.exception.HopTransformException; +import org.apache.hop.core.logging.LogChannel; +import org.apache.hop.core.logging.LoggingObject; +import org.apache.hop.core.metadata.SerializableMetadataProvider; +import org.apache.hop.core.plugins.PluginRegistry; +import org.apache.hop.core.plugins.TransformPluginType; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.row.IValueMeta; +import org.apache.hop.core.row.JsonRowMeta; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.core.vfs.HopVfs; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.PipelineHopMeta; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.RowProducer; +import org.apache.hop.pipeline.SingleThreadedPipelineExecutor; +import org.apache.hop.pipeline.TransformWithMappingMeta; +import org.apache.hop.pipeline.engines.local.LocalPipelineEngine; +import org.apache.hop.pipeline.transform.BaseTransform; +import org.apache.hop.pipeline.transform.ITransform; +import org.apache.hop.pipeline.transform.ITransformMeta; +import org.apache.hop.pipeline.transform.RowAdapter; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.pipeline.transform.TransformMetaDataCombi; +import org.apache.hop.pipeline.transforms.dummy.DummyMeta; +import org.apache.hop.pipeline.transforms.file.BaseFileInputMeta; +import org.apache.hop.pipeline.transforms.injector.InjectorField; +import org.apache.hop.pipeline.transforms.injector.InjectorMeta; +import org.apache.hop.spark.execution.SparkTransformExecutionSampling; +import org.apache.hop.spark.pkg.SparkProjectPackage; +import org.apache.hop.spark.util.SparkConst; +import org.apache.spark.TaskContext; +import org.apache.spark.api.java.function.MapPartitionsFunction; +import org.apache.spark.broadcast.Broadcast; +import org.apache.spark.sql.Row; + +/** + * Executes a single Hop transform once per Spark partition via {@link + * SingleThreadedPipelineExecutor}. Metadata is passed as serializable strings (XML/JSON) so + * executors never receive live Hop graph objects. + */ +public class HopMapPartitionsFn implements MapPartitionsFunction, Serializable { + private static final long serialVersionUID = 4L; + + /** Publish progress at least every this many input rows. */ + private static final int METRICS_ROW_INTERVAL = 1000; + + /** Publish progress at least every this many milliseconds. */ + private static final long METRICS_TIME_INTERVAL_MS = 1000L; + + private final List variableValues; + private final String metastoreJson; + private final String transformName; + private final String transformPluginId; + private final String transformMetaInterfaceXml; + private final String inputRowMetaJson; + private final String outputRowMetaJson; + private final boolean inputTransform; + private final List targetTransforms; + + /** Names of info/side transforms (Stream Lookup "from", etc.). */ + private final List infoTransforms; + + /** Parallel to {@link #infoTransforms}: row meta JSON per info stream. */ + private final List infoRowMetaJsons; + + /** Parallel to {@link #infoTransforms}: broadcast Hop rows for each info stream. */ + private final List>> infoBroadcasts; + + private final SparkTransformMetricsAccumulator metricsAccumulator; + private final SparkExecutionDataAccumulator sampleDataAccumulator; + private final String runConfigName; + private final String parentLogChannelId; + private final String dataSamplersJson; + + public HopMapPartitionsFn( + List variableValues, + String metastoreJson, + String transformName, + String transformPluginId, + String transformMetaInterfaceXml, + String inputRowMetaJson, + String outputRowMetaJson, + boolean inputTransform, + List targetTransforms) { + this( + variableValues, + metastoreJson, + transformName, + transformPluginId, + transformMetaInterfaceXml, + inputRowMetaJson, + outputRowMetaJson, + inputTransform, + targetTransforms, + null, + null, + null, + null, + null, + null, + null, + null); + } + + public HopMapPartitionsFn( + List variableValues, + String metastoreJson, + String transformName, + String transformPluginId, + String transformMetaInterfaceXml, + String inputRowMetaJson, + String outputRowMetaJson, + boolean inputTransform, + List targetTransforms, + SparkTransformMetricsAccumulator metricsAccumulator) { + this( + variableValues, + metastoreJson, + transformName, + transformPluginId, + transformMetaInterfaceXml, + inputRowMetaJson, + outputRowMetaJson, + inputTransform, + targetTransforms, + null, + null, + null, + metricsAccumulator, + null, + null, + null, + null); + } + + public HopMapPartitionsFn( + List variableValues, + String metastoreJson, + String transformName, + String transformPluginId, + String transformMetaInterfaceXml, + String inputRowMetaJson, + String outputRowMetaJson, + boolean inputTransform, + List targetTransforms, + SparkTransformMetricsAccumulator metricsAccumulator, + String runConfigName, + String parentLogChannelId, + String dataSamplersJson) { + this( + variableValues, + metastoreJson, + transformName, + transformPluginId, + transformMetaInterfaceXml, + inputRowMetaJson, + outputRowMetaJson, + inputTransform, + targetTransforms, + null, + null, + null, + metricsAccumulator, + null, + runConfigName, + parentLogChannelId, + dataSamplersJson); + } + + public HopMapPartitionsFn( + List variableValues, + String metastoreJson, + String transformName, + String transformPluginId, + String transformMetaInterfaceXml, + String inputRowMetaJson, + String outputRowMetaJson, + boolean inputTransform, + List targetTransforms, + SparkTransformMetricsAccumulator metricsAccumulator, + SparkExecutionDataAccumulator sampleDataAccumulator, + String runConfigName, + String parentLogChannelId, + String dataSamplersJson) { + this( + variableValues, + metastoreJson, + transformName, + transformPluginId, + transformMetaInterfaceXml, + inputRowMetaJson, + outputRowMetaJson, + inputTransform, + targetTransforms, + null, + null, + null, + metricsAccumulator, + sampleDataAccumulator, + runConfigName, + parentLogChannelId, + dataSamplersJson); + } + + public HopMapPartitionsFn( + List variableValues, + String metastoreJson, + String transformName, + String transformPluginId, + String transformMetaInterfaceXml, + String inputRowMetaJson, + String outputRowMetaJson, + boolean inputTransform, + List targetTransforms, + List infoTransforms, + List infoRowMetaJsons, + List>> infoBroadcasts, + SparkTransformMetricsAccumulator metricsAccumulator, + SparkExecutionDataAccumulator sampleDataAccumulator, + String runConfigName, + String parentLogChannelId, + String dataSamplersJson) { + this.variableValues = variableValues != null ? variableValues : List.of(); + this.metastoreJson = metastoreJson; + this.transformName = transformName; + this.transformPluginId = transformPluginId; + this.transformMetaInterfaceXml = transformMetaInterfaceXml; + this.inputRowMetaJson = inputRowMetaJson; + this.outputRowMetaJson = outputRowMetaJson; + this.inputTransform = inputTransform; + this.targetTransforms = targetTransforms != null ? targetTransforms : List.of(); + this.infoTransforms = infoTransforms != null ? infoTransforms : List.of(); + this.infoRowMetaJsons = infoRowMetaJsons != null ? infoRowMetaJsons : List.of(); + this.infoBroadcasts = infoBroadcasts != null ? infoBroadcasts : List.of(); + this.metricsAccumulator = metricsAccumulator; + this.sampleDataAccumulator = sampleDataAccumulator; + this.runConfigName = runConfigName; + this.parentLogChannelId = parentLogChannelId; + this.dataSamplersJson = dataSamplersJson; + } + + @Override + public Iterator call(Iterator input) throws Exception { + int copyNr = partitionId(); + String host = localHost(); + long partitionStartMs = System.currentTimeMillis(); + ITransform mainTransform = null; + SparkTransformExecutionSampling sampling = null; + try { + SparkHop.init(); + + IHopMetadataProvider metadataProvider = new SerializableMetadataProvider(metastoreJson); + IVariables variables = new Variables(); + for (SparkVariableValue variableValue : variableValues) { + if (StringUtils.isNotEmpty(variableValue.getVariable())) { + variables.setVariable(variableValue.getVariable(), variableValue.getValue()); + } + } + // Spark project package: extract once per JVM into java.io.tmpdir and set PROJECT_HOME + // so Simple Mapping / Pipeline Executor path loads work on executors. + SparkProjectPackage.ensureMaterializedOnWorker(variables); + validateProjectHomeOnWorker(variables); + // Partition-scoped internal variables for classic I/O filenames (e.g. Text File Output + // with ${Internal.Transform.ID}). Readable stable ID = transform name + partition id. + applyPartitionInternalVariables(variables, transformName, copyNr); + // Nested Workflow/Pipeline Executor parent linkage for execution perspective drill-down. + applySparkTransformOwnerId(variables, parentLogChannelId, transformName, copyNr); + + IRowMeta inputRowMeta = + StringUtils.isNotEmpty(inputRowMetaJson) + ? JsonRowMeta.fromJson(inputRowMetaJson) + : new org.apache.hop.core.row.RowMeta(); + IRowMeta outputRowMeta = JsonRowMeta.fromJson(outputRowMetaJson); + + PipelineMeta pipelineMeta = new PipelineMeta(); + pipelineMeta.setName(transformName); + pipelineMeta.setPipelineType(PipelineMeta.PipelineType.SingleThreaded); + pipelineMeta.setMetadataProvider(metadataProvider); + + if (infoTransforms.size() != infoRowMetaJsons.size() + || infoTransforms.size() != infoBroadcasts.size()) { + throw new HopException( + "Info stream configuration mismatch for transform '" + + transformName + + "': names=" + + infoTransforms.size() + + " metas=" + + infoRowMetaJsons.size() + + " broadcasts=" + + infoBroadcasts.size()); + } + + List infoRowMetas = new ArrayList<>(); + for (String infoRowMetaJson : infoRowMetaJsons) { + infoRowMetas.add(JsonRowMeta.fromJson(infoRowMetaJson)); + } + + TransformMeta mainInjectorTransformMeta = null; + if (!inputTransform) { + mainInjectorTransformMeta = + createInjectorTransform( + pipelineMeta, SparkConst.INJECTOR_TRANSFORM_NAME, inputRowMeta, 200, 200); + } + + // Injectors for info/side streams (Stream Lookup, Validator, …) — Beam side-input pattern + List infoTransformMetas = new ArrayList<>(); + for (int i = 0; i < infoTransforms.size(); i++) { + infoTransformMetas.add( + createInjectorTransform( + pipelineMeta, infoTransforms.get(i), infoRowMetas.get(i), 200, 350 + 150 * i)); + } + + int targetLocationY = 200; + List targetTransformMetas = new ArrayList<>(); + for (String targetTransform : targetTransforms) { + DummyMeta dummyMeta = new DummyMeta(); + TransformMeta targetTransformMeta = new TransformMeta(targetTransform, dummyMeta); + targetTransformMeta.setLocation(600, targetLocationY); + targetLocationY += 150; + targetTransformMetas.add(targetTransformMeta); + pipelineMeta.addTransform(targetTransformMeta); + } + + PluginRegistry registry = PluginRegistry.getInstance(); + ITransformMeta iTransformMeta = + registry.loadClass(TransformPluginType.class, transformPluginId, ITransformMeta.class); + if (iTransformMeta == null) { + throw new HopException( + "Unable to load transform plugin with ID " + + transformPluginId + + ", this plugin isn't in the plugin registry or classpath"); + } + + HopSparkUtil.loadTransformMetadataFromXml( + transformName, iTransformMeta, transformMetaInterfaceXml, metadataProvider); + + // Resolve ${PROJECT_HOME}/… mapping/executor filenames on this JVM after package materialize + // so Simple Mapping / Pipeline Executor open the child .hpl from the local extract. + resolveNestedPipelineFilename(iTransformMeta, variables, transformName); + + // Text File Input / Excel / etc. accept filenames from a *main* hop (not an INFO stream) + // and call findInputRowSet(accept_transform_name). The mini-pipeline only has the main + // injector named _INJECTOR_, so re-bind accept_transform_name to that injector. Filename + // rows must also be fully loaded before processRow drains the source (see below). + boolean acceptFilenamesFromMain = bindAcceptingFilenamesToMainInjector(iTransformMeta); + + TransformMeta transformMeta = new TransformMeta(transformName, iTransformMeta); + transformMeta.setTransformPluginId(transformPluginId); + transformMeta.setLocation(400, 200); + pipelineMeta.addTransform(transformMeta); + // Give CurrentDirectoryResolver a folder under PROJECT_HOME when resolving relatives + String projectHome = variables.getVariable("PROJECT_HOME"); + if (StringUtils.isNotEmpty(projectHome)) { + pipelineMeta.setFilename( + projectHome + + (projectHome.endsWith("/") || projectHome.endsWith("\\") ? "" : File.separator) + + "_spark_map_partitions.hpl"); + } + if (!inputTransform) { + pipelineMeta.addPipelineHop(new PipelineHopMeta(mainInjectorTransformMeta, transformMeta)); + } + for (TransformMeta infoTransformMeta : infoTransformMetas) { + pipelineMeta.addPipelineHop(new PipelineHopMeta(infoTransformMeta, transformMeta)); + } + for (TransformMeta targetTransformMeta : targetTransformMetas) { + pipelineMeta.addPipelineHop(new PipelineHopMeta(transformMeta, targetTransformMeta)); + } + + // After injectors exist so Stream Lookup "from" can bind to the info injector by name + iTransformMeta.searchInfoAndTargetTransforms(pipelineMeta.getTransforms()); + + LocalPipelineEngine pipeline = + new LocalPipelineEngine( + pipelineMeta, variables, new LoggingObject("apache-spark-transform")); + pipeline.setMetadataProvider(metadataProvider); + pipeline + .getPipelineRunConfiguration() + .setName("spark-transform-local (" + transformName + ")"); + try { + pipeline.prepareExecution(); + } catch (Exception e) { + throw new HopException( + "Failed to prepare transform '" + + transformName + + "' (plugin id=" + + transformPluginId + + ") on Spark partition " + + copyNr + + " host=" + + host + + " PROJECT_HOME=" + + variables.getVariable("PROJECT_HOME") + + " HOP_SPARK_PROJECT_PACKAGE=" + + variables.getVariable(SparkProjectPackage.VAR_PACKAGE_URI), + e); + } + + RowProducer rowProducer = null; + TransformMetaDataCombi mainInjectorCombi = null; + if (!inputTransform) { + rowProducer = pipeline.addRowProducer(SparkConst.INJECTOR_TRANSFORM_NAME, 0); + mainInjectorCombi = findCombi(pipeline, SparkConst.INJECTOR_TRANSFORM_NAME); + } + List infoRowProducers = new ArrayList<>(); + for (String infoTransform : infoTransforms) { + infoRowProducers.add(pipeline.addRowProducer(infoTransform, 0)); + } + + // Non-blocking getRow/putRow (Beam pattern). Default handler busy-waits on empty hops and + // deadlocks Stream Lookup after readLookupValues when the main hop is empty in this thread. + for (TransformMetaDataCombi c : pipeline.getTransforms()) { + if (c.transform instanceof BaseTransform baseTransform) { + baseTransform.setRowHandler(new SparkRowHandler(baseTransform)); + } + } + + TransformMetaDataCombi transformCombi = findCombi(pipeline, transformName); + mainTransform = transformCombi.transform; + publishMetrics(mainTransform, copyNr, host, partitionStartMs, true, false); + + // Execution data sampling → driver accumulator + optional local registerData + sampling = + new SparkTransformExecutionSampling( + transformName, parentLogChannelId, copyNr, sampleDataAccumulator); + try { + sampling.lookup(variables, metadataProvider, runConfigName, dataSamplersJson); + if (sampling.isActive()) { + sampling.registerExecutingTransform(pipeline); + sampling.attach(variables, pipeline, mainTransform, inputRowMeta, outputRowMeta); + } + } catch (Throwable sampleEx) { + // Non-fatal: pipeline must still process data. Catch Throwable so LinkageError + // (Jackson dual classloaders on local[*]) cannot abort the Spark stage. + LogChannel.GENERAL.logError( + "Execution data sampling disabled for transform '" + + transformName + + "' on partition " + + copyNr + + " (non-fatal): " + + sampleEx.getMessage(), + sampleEx instanceof Exception ex ? ex : new Exception(sampleEx)); + sampling = null; + } + + // Untagged main-path capture (no target streams) or per-target capture lists (Beam pattern) + List resultRows = new ArrayList<>(); + List> targetResultRowsList = new ArrayList<>(); + final boolean multiTarget = !targetTransforms.isEmpty(); + if (!multiTarget) { + transformCombi.transform.addRowListener( + new RowAdapter() { + @Override + public void rowWrittenEvent(IRowMeta rowMeta, Object[] row) + throws HopTransformException { + resultRows.add(row); + } + }); + } else { + for (String targetTransform : targetTransforms) { + List targetResultRows = new ArrayList<>(); + targetResultRowsList.add(targetResultRows); + TransformMetaDataCombi targetCombi = findCombi(pipeline, targetTransform); + targetCombi.transform.addRowListener( + new RowAdapter() { + @Override + public void rowReadEvent(IRowMeta rowMeta, Object[] row) + throws HopTransformException { + targetResultRows.add(row); + } + }); + } + } + + SingleThreadedPipelineExecutor executor = new SingleThreadedPipelineExecutor(pipeline); + if (!executor.init()) { + throw new HopException( + "Error initializing single-threaded executor for transform '" + transformName + "'"); + } + // Init calls setInternalVariables() with log-channel UUID / copy 0 — re-apply partition + // scope and beamContext so Text File Output auto-suffix and ${Internal.Transform.*} work. + applySparkParallelFileContext(pipeline, transformName, copyNr); + + pipeline.startThreads(); + + // Load info streams once per partition (before main rows), same pattern as Beam TransformFn: + // put each side-input row through the info Injector via RowProducer, then processRow so the + // hop into Stream Lookup (etc.) is filled. finished() + one more processRow flags the + // injector done so readLookupValues / getRowFrom see isDone() and do not busy-wait. + for (int i = 0; i < infoTransforms.size(); i++) { + String infoName = infoTransforms.get(i); + IRowMeta infoRowMeta = infoRowMetas.get(i); + List infoData = + infoBroadcasts.get(i) != null ? infoBroadcasts.get(i).value() : Collections.emptyList(); + RowProducer infoRowProducer = infoRowProducers.get(i); + TransformMetaDataCombi infoCombi = findCombi(pipeline, infoName); + for (Object[] infoRow : infoData) { + infoRowProducer.putRow(infoRowMeta, infoRow); + infoCombi.transform.processRow(); + } + infoRowProducer.finished(); + infoCombi.transform.processRow(); + } + + List output = new ArrayList<>(); + MetricsThrottle throttle = new MetricsThrottle(); + final SparkTransformExecutionSampling samplingRef = sampling; + + if (inputTransform) { + // Source transform: drive until finished with no external input + driveUntilDone( + pipeline, + executor, + output, + outputRowMeta, + multiTarget, + resultRows, + targetTransforms, + targetResultRowsList, + throttle, + mainTransform, + copyNr, + host, + partitionStartMs, + samplingRef); + } else if (acceptFilenamesFromMain) { + // Accept-filenames (Text File Input, Excel, …): + // 1) Pre-load every partition row (filenames) into the main injector and mark finished. + // 2) Drive the file reader with processRow() directly until it returns false. + // + // We cannot use SingleThreadedPipelineExecutor.oneIteration() after filenames are + // drained: with a non-empty input rowset list it only calls processRow once per + // remaining hop size. After the first processRow consumes all filenames, size is 0 + // and further oneIteration() calls never advance the reader (infinite stall after + // openNextFile/createReader — the log line users see just before hang). + long rowsSeen = 0; + while (input.hasNext()) { + Row sparkRow = input.next(); + Object[] hopRow = HopSparkRowConverter.toHopRow(inputRowMeta, sparkRow); + rowProducer.putRow(inputRowMeta, hopRow, false); + if (mainInjectorCombi != null) { + mainInjectorCombi.transform.processRow(); + } + rowsSeen++; + } + if (rowProducer != null) { + rowProducer.finished(); + if (mainInjectorCombi != null) { + mainInjectorCombi.transform.processRow(); + } + } + if (rowsSeen > 0 || throttle.shouldPublish(0)) { + publishMetrics(mainTransform, copyNr, host, partitionStartMs, true, false); + } + driveAcceptFilenamesUntilDone( + pipeline, + mainTransform, + output, + outputRowMeta, + multiTarget, + resultRows, + targetTransforms, + targetResultRowsList, + throttle, + copyNr, + host, + partitionStartMs, + samplingRef); + } else { + long rowsSeen = 0; + while (input.hasNext()) { + Row sparkRow = input.next(); + Object[] hopRow = HopSparkRowConverter.toHopRow(inputRowMeta, sparkRow); + clearCapture(resultRows, targetResultRowsList); + rowProducer.putRow(inputRowMeta, hopRow, false); + // Forward the main row onto the hop before Stream Lookup's info-first processRow + // calls getRow() for the main stream (same timing Beam gets from topo order + + // non-blocking handler). + if (mainInjectorCombi != null) { + mainInjectorCombi.transform.processRow(); + } + executor.oneIteration(); + appendCapturedRows( + output, + outputRowMeta, + multiTarget, + resultRows, + targetTransforms, + targetResultRowsList); + rowsSeen++; + if (throttle.shouldPublish(1) || rowsSeen % METRICS_ROW_INTERVAL == 0) { + publishMetrics(mainTransform, copyNr, host, partitionStartMs, true, false); + flushSamplesQuietly(samplingRef, false); + } + } + if (rowProducer != null) { + rowProducer.finished(); + if (mainInjectorCombi != null) { + mainInjectorCombi.transform.processRow(); + } + clearCapture(resultRows, targetResultRowsList); + executor.oneIteration(); + appendCapturedRows( + output, + outputRowMeta, + multiTarget, + resultRows, + targetTransforms, + targetResultRowsList); + } + } + + if (pipeline.getErrors() > 0) { + publishMetrics(mainTransform, copyNr, host, partitionStartMs, false, true); + throw new HopException( + "Errors detected while executing transform '" + + transformName + + "' on a Spark partition"); + } + + executor.dispose(); + publishMetrics(mainTransform, copyNr, host, partitionStartMs, false, true); + if (sampling != null) { + sampling.close(); + sampling = null; + } + return output.iterator(); + } catch (Exception e) { + if (mainTransform != null) { + try { + publishMetrics(mainTransform, copyNr, host, partitionStartMs, false, true); + } catch (Exception ignored) { + // best-effort metrics on failure + } + } else { + publishErrorSlice(copyNr, host, partitionStartMs); + } + if (sampling != null) { + try { + sampling.close(); + } catch (Exception ignored) { + // best-effort + } + } + throw new HopRuntimeException( + "Error executing Hop transform '" + transformName + "' in Spark mapPartitions", e); + } + } + + private static void flushSamplesQuietly( + SparkTransformExecutionSampling sampling, boolean finished) { + if (sampling == null || !sampling.isActive()) { + return; + } + try { + sampling.sendSamplesToLocation(finished); + } catch (Exception e) { + LogChannel.GENERAL.logError( + "Error flushing transform samples to execution location (non-fatal)", e); + } + } + + private void publishMetrics( + ITransform transform, + int copyNr, + String host, + long startTimeMs, + boolean running, + boolean finished) { + if (metricsAccumulator == null || transform == null) { + return; + } + long endTimeMs = finished ? System.currentTimeMillis() : 0L; + metricsAccumulator.add( + new SparkTransformMetricSlice( + transformName, + copyNr, + host, + transform.getLinesRead(), + transform.getLinesWritten(), + transform.getLinesInput(), + transform.getLinesOutput(), + transform.getErrors(), + running, + finished, + startTimeMs, + endTimeMs)); + } + + private void publishErrorSlice(int copyNr, String host, long startTimeMs) { + if (metricsAccumulator == null) { + return; + } + long now = System.currentTimeMillis(); + long start = startTimeMs > 0 ? startTimeMs : now; + metricsAccumulator.add( + new SparkTransformMetricSlice( + transformName, copyNr, host, 0, 0, 0, 0, 1, false, true, start, now)); + } + + private static int partitionId() { + TaskContext ctx = TaskContext.get(); + return ctx != null ? ctx.partitionId() : 0; + } + + private static String localHost() { + try { + return InetAddress.getLocalHost().getHostName(); + } catch (Exception e) { + return null; + } + } + + private static TransformMeta createInjectorTransform( + PipelineMeta pipelineMeta, + String injectorTransformName, + IRowMeta injectorRowMeta, + int x, + int y) { + InjectorMeta injectorMeta = new InjectorMeta(); + for (IValueMeta valueMeta : injectorRowMeta.getValueMetaList()) { + injectorMeta + .getInjectorFields() + .add( + new InjectorField( + valueMeta.getName(), + valueMeta.getTypeDesc(), + Integer.toString(valueMeta.getLength()), + Integer.toString(valueMeta.getPrecision()))); + } + TransformMeta injectorTransformMeta = new TransformMeta(injectorTransformName, injectorMeta); + injectorTransformMeta.setLocation(x, y); + pipelineMeta.addTransform(injectorTransformMeta); + return injectorTransformMeta; + } + + private static TransformMetaDataCombi findCombi(LocalPipelineEngine pipeline, String name) { + for (TransformMetaDataCombi combi : pipeline.getTransforms()) { + if (combi.transformName.equals(name)) { + return combi; + } + } + throw new HopRuntimeException( + "Configuration error, transform '" + name + "' not found in mini-pipeline"); + } + + /** + * Sets partition-scoped {@code Internal.Transform.*} variables on a variable space (readable ID = + * {@code transformName-partitionId}). + */ + static void applyPartitionInternalVariables( + IVariables variables, String transformName, int partitionId) { + if (variables == null) { + return; + } + String partitionIdStr = Integer.toString(partitionId); + String instanceId = transformName + "-" + partitionIdStr; + variables.setVariable(Const.INTERNAL_VARIABLE_TRANSFORM_NAME, transformName); + variables.setVariable(Const.INTERNAL_VARIABLE_TRANSFORM_COPYNR, partitionIdStr); + variables.setVariable(Const.INTERNAL_VARIABLE_TRANSFORM_ID, instanceId); + variables.setVariable(Const.INTERNAL_VARIABLE_TRANSFORM_BUNDLE_NR, partitionIdStr); + } + + /** + * Publish the same synthetic owner id used by {@link + * org.apache.hop.spark.execution.SparkTransformExecutionSampling} so nested engines can parent + * their execution-info rows for GUI drill-down. + */ + static void applySparkTransformOwnerId( + IVariables variables, String parentLogChannelId, String transformName, int partitionId) { + if (variables == null || StringUtils.isEmpty(transformName)) { + return; + } + String parent = + StringUtils.isNotEmpty(parentLogChannelId) ? parentLogChannelId : "spark-pipeline"; + String ownerId = parent + "|" + transformName + "|" + partitionId; + variables.setVariable(SparkConst.VAR_TRANSFORM_OWNER_ID, ownerId); + } + + /** + * After package materialize, resolve nested pipeline paths (Simple Mapping, Pipeline Executor, …) + * to absolute files on this JVM and verify they exist. + */ + static void resolveNestedPipelineFilename( + ITransformMeta iTransformMeta, IVariables variables, String transformName) + throws HopException { + if (!(iTransformMeta instanceof TransformWithMappingMeta mappingMeta)) { + return; + } + String filename = mappingMeta.getFilename(); + if (StringUtils.isEmpty(filename)) { + throw new HopException( + "Transform '" + + transformName + + "' is a nested pipeline transform but has no filename configured"); + } + String resolved = variables != null ? variables.resolve(filename) : filename; + if (StringUtils.isEmpty(resolved) || resolved.contains("${") || resolved.contains("%")) { + throw new HopException( + "Could not resolve nested pipeline filename for transform '" + + transformName + + "': '" + + filename + + "' → '" + + resolved + + "' (PROJECT_HOME=" + + (variables != null ? variables.getVariable("PROJECT_HOME") : null) + + ", package=" + + (variables != null + ? variables.getVariable(SparkProjectPackage.VAR_PACKAGE_URI) + : null) + + "). Deploy with Upload Spark project package so PROJECT_HOME is set on every" + + " executor."); + } + File f = new File(resolved); + boolean exists = f.isFile(); + if (!exists) { + try { + exists = HopVfs.fileExists(resolved); + } catch (Exception e) { + throw new HopException( + "Nested pipeline file not found for transform '" + + transformName + + "': " + + resolved + + " (from '" + + filename + + "')", + e); + } + } + if (!exists) { + throw new HopException( + "Nested pipeline file not found for transform '" + + transformName + + "': " + + resolved + + " (from '" + + filename + + "'). Check that hop-spark-package.zip contains this path under project/ " + + "(e.g. project/mappings/add-uuid.hpl) and re-run Deploy & run with package" + + " upload."); + } + mappingMeta.setFilename(resolved); + } + + /** + * After package materialize, ensure PROJECT_HOME is a real directory on this JVM. Detects the + * common failure where only the driver extract path was broadcast and workers never re-extracted + * the package (nested Simple Mapping / Pipeline Executor then fail to open {@code + * ${PROJECT_HOME}/…}). + */ + static void validateProjectHomeOnWorker(IVariables variables) throws HopException { + if (variables == null) { + return; + } + String packageUri = variables.getVariable(SparkProjectPackage.VAR_PACKAGE_URI); + String sparkFile = variables.getVariable(SparkProjectPackage.VAR_PACKAGE_SPARK_FILE); + String projectHome = variables.getVariable("PROJECT_HOME"); + if (StringUtils.isEmpty(packageUri) + && StringUtils.isEmpty(sparkFile) + && StringUtils.isEmpty(projectHome)) { + return; + } + if (StringUtils.isNotEmpty(projectHome)) { + File home = new File(projectHome); + if (home.isDirectory()) { + return; + } + throw new HopException( + "PROJECT_HOME is not a directory on this Spark executor: " + + projectHome + + ". Package URI=" + + packageUri + + ", SparkFiles basename=" + + sparkFile + + ". The driver extract under /tmp is not visible on workers — re-run with a fat jar" + + " that stages the project package via SparkFiles (local copy + addFile) and/or a" + + " UC Volume path every node can open. Nested definition files (mappings," + + " sub-pipelines) live under project/ inside the package zip."); + } + if (StringUtils.isNotEmpty(packageUri) || StringUtils.isNotEmpty(sparkFile)) { + throw new HopException( + "HOP_SPARK_PROJECT_PACKAGE is set (" + + packageUri + + ", sparkFile=" + + sparkFile + + ") but PROJECT_HOME was not set after package materialization on this executor."); + } + } + + /** + * Beam-parity parallel file context for classic I/O in mapPartitions: enable {@code beamContext} + * (Text File Output auto-appends {@code _transformId_bundleNr}) and re-apply partition-scoped + * internal variables after transform init overwrote them with log-channel UUIDs. + */ + static void applySparkParallelFileContext( + LocalPipelineEngine pipeline, String primaryTransformName, int partitionId) { + if (pipeline == null) { + return; + } + String partitionIdStr = Integer.toString(partitionId); + String ownerId = pipeline.getVariable(SparkConst.VAR_TRANSFORM_OWNER_ID); + for (TransformMetaDataCombi combi : pipeline.getTransforms()) { + if (combi.data != null) { + combi.data.setBeamContext(true); + combi.data.setBeamBundleNr(partitionId); + } + if (combi.transform instanceof BaseTransform baseTransform) { + String name = + StringUtils.isNotEmpty(combi.transformName) + ? combi.transformName + : primaryTransformName; + // Prefer the Hop transform name for the main transform (filename templates) + if (primaryTransformName != null && primaryTransformName.equals(combi.transformName)) { + name = primaryTransformName; + } + String instanceId = name + "-" + partitionIdStr; + baseTransform.setVariable(Const.INTERNAL_VARIABLE_TRANSFORM_NAME, name); + baseTransform.setVariable(Const.INTERNAL_VARIABLE_TRANSFORM_COPYNR, partitionIdStr); + baseTransform.setVariable(Const.INTERNAL_VARIABLE_TRANSFORM_ID, instanceId); + baseTransform.setVariable(Const.INTERNAL_VARIABLE_TRANSFORM_BUNDLE_NR, partitionIdStr); + if (StringUtils.isNotEmpty(ownerId)) { + baseTransform.setVariable(SparkConst.VAR_TRANSFORM_OWNER_ID, ownerId); + } + } + } + } + + /** + * When a file input transform accepts filenames from a previous hop, re-point {@code + * accept_transform_name} at the mini-pipeline main injector so {@code findInputRowSet} succeeds. + * Covers {@link BaseFileInputMeta} (Text File Input, JSON, VCard, …) and Excel-style metas with + * {@code isAcceptingFilenames}/{@code setAcceptingTransformName}. + * + * @return true when the transform consumes filename rows from the main injector this way + */ + static boolean bindAcceptingFilenamesToMainInjector(ITransformMeta meta) { + if (meta == null) { + return false; + } + if (meta instanceof BaseFileInputMeta baseFile) { + if (baseFile.isAcceptingFilenames()) { + baseFile.setAcceptingTransformName(SparkConst.INJECTOR_TRANSFORM_NAME); + return true; + } + return false; + } + // ExcelInputMeta (and similar) are not BaseFileInputMeta but use the same contract + try { + Object accepting = meta.getClass().getMethod("isAcceptingFilenames").invoke(meta); + if (Boolean.TRUE.equals(accepting)) { + meta.getClass() + .getMethod("setAcceptingTransformName", String.class) + .invoke(meta, SparkConst.INJECTOR_TRANSFORM_NAME); + return true; + } + } catch (ReflectiveOperationException ignored) { + // not an accept-filenames transform + } + return false; + } + + /** + * Drive the single-threaded mini-pipeline until it finishes (source transforms with no main input + * hop — Row Generator, Get File Names, etc.). + */ + private void driveUntilDone( + LocalPipelineEngine pipeline, + SingleThreadedPipelineExecutor executor, + List output, + IRowMeta outputRowMeta, + boolean multiTarget, + List resultRows, + List targetTransforms, + List> targetResultRowsList, + MetricsThrottle throttle, + ITransform mainTransform, + int copyNr, + String host, + long partitionStartMs, + SparkTransformExecutionSampling samplingRef) + throws HopException { + boolean more = true; + while (more && !pipeline.isFinished() && pipeline.getErrors() == 0) { + clearCapture(resultRows, targetResultRowsList); + more = executor.oneIteration(); + appendCapturedRows( + output, outputRowMeta, multiTarget, resultRows, targetTransforms, targetResultRowsList); + if (throttle.shouldPublish(resultRows.size())) { + publishMetrics(mainTransform, copyNr, host, partitionStartMs, true, false); + flushSamplesQuietly(samplingRef, false); + } + } + } + + /** + * After filename rows are pre-loaded onto the main injector, call {@code processRow()} on the + * file-input transform until it finishes reading content. + * + *

{@link SingleThreadedPipelineExecutor#oneIteration()} only schedules {@code processRow} once + * per remaining input-rowset size when the transform has input hops. After accept-filenames + * drains those rows, size is 0 and further iterations never advance the reader. + */ + private void driveAcceptFilenamesUntilDone( + LocalPipelineEngine pipeline, + ITransform mainTransform, + List output, + IRowMeta outputRowMeta, + boolean multiTarget, + List resultRows, + List targetTransforms, + List> targetResultRowsList, + MetricsThrottle throttle, + int copyNr, + String host, + long partitionStartMs, + SparkTransformExecutionSampling samplingRef) + throws HopException { + boolean more = true; + long contentRows = 0; + while (more && !pipeline.isFinished() && pipeline.getErrors() == 0) { + clearCapture(resultRows, targetResultRowsList); + more = mainTransform.processRow(); + appendCapturedRows( + output, outputRowMeta, multiTarget, resultRows, targetTransforms, targetResultRowsList); + contentRows += resultRows.size(); + if (throttle.shouldPublish(resultRows.size()) || contentRows % METRICS_ROW_INTERVAL == 0) { + publishMetrics(mainTransform, copyNr, host, partitionStartMs, true, false); + flushSamplesQuietly(samplingRef, false); + } + } + } + + private static void clearCapture( + List resultRows, List> targetResultRowsList) { + resultRows.clear(); + for (List list : targetResultRowsList) { + list.clear(); + } + } + + private static void appendCapturedRows( + List output, + IRowMeta outputRowMeta, + boolean multiTarget, + List resultRows, + List targetTransforms, + List> targetResultRowsList) + throws HopException { + if (!multiTarget) { + for (Object[] hopRow : resultRows) { + output.add(HopSparkRowConverter.toSparkRow(outputRowMeta, hopRow)); + } + return; + } + for (int t = 0; t < targetTransforms.size(); t++) { + String tag = targetTransforms.get(t); + for (Object[] hopRow : targetResultRowsList.get(t)) { + output.add(HopSparkRowConverter.toTaggedSparkRow(tag, outputRowMeta, hopRow)); + } + } + } + + /** Throttles metric publishes by wall-clock time (row interval handled by caller). */ + private static final class MetricsThrottle implements Serializable { + private static final long serialVersionUID = 1L; + private long lastPublishMs = 0L; + + boolean shouldPublish(int rowsThisBatch) { + long now = System.currentTimeMillis(); + if (lastPublishMs == 0L || now - lastPublishMs >= METRICS_TIME_INTERVAL_MS) { + lastPublishMs = now; + return true; + } + return false; + } + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/HopSparkRowConverter.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/HopSparkRowConverter.java new file mode 100644 index 00000000000..55a0a462b4c --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/HopSparkRowConverter.java @@ -0,0 +1,225 @@ +/* + * 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.hop.spark.core; + +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.row.IValueMeta; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.RowFactory; +import org.apache.spark.sql.types.DataType; +import org.apache.spark.sql.types.DataTypes; +import org.apache.spark.sql.types.DecimalType; +import org.apache.spark.sql.types.Metadata; +import org.apache.spark.sql.types.StructField; +import org.apache.spark.sql.types.StructType; + +/** + * Bidirectional conversion between Hop {@link IRowMeta}/{@code Object[]} and Spark {@link + * StructType}/{@link Row}. + */ +public final class HopSparkRowConverter { + + private HopSparkRowConverter() {} + + public static StructType toStructType(IRowMeta rowMeta) throws HopException { + if (rowMeta == null) { + return new StructType(); + } + List fields = new ArrayList<>(rowMeta.size()); + for (int i = 0; i < rowMeta.size(); i++) { + IValueMeta valueMeta = rowMeta.getValueMeta(i); + fields.add( + new StructField(valueMeta.getName(), toDataType(valueMeta), true, Metadata.empty())); + } + return new StructType(fields.toArray(new StructField[0])); + } + + public static DataType toDataType(IValueMeta valueMeta) throws HopException { + return switch (valueMeta.getType()) { + case IValueMeta.TYPE_STRING, IValueMeta.TYPE_INET -> DataTypes.StringType; + case IValueMeta.TYPE_INTEGER -> DataTypes.LongType; + case IValueMeta.TYPE_NUMBER -> DataTypes.DoubleType; + case IValueMeta.TYPE_BIGNUMBER -> DataTypes.createDecimalType(38, 18); + case IValueMeta.TYPE_BOOLEAN -> DataTypes.BooleanType; + case IValueMeta.TYPE_DATE -> DataTypes.TimestampType; + case IValueMeta.TYPE_TIMESTAMP -> DataTypes.TimestampType; + case IValueMeta.TYPE_BINARY -> DataTypes.BinaryType; + case IValueMeta.TYPE_NONE, IValueMeta.TYPE_SERIALIZABLE -> DataTypes.StringType; + default -> + throw new HopException( + "Unsupported Hop type for Spark conversion: " + + valueMeta.getTypeDesc() + + " (" + + valueMeta.getName() + + ")"); + }; + } + + public static Row toSparkRow(IRowMeta rowMeta, Object[] hopRow) throws HopException { + Object[] values = new Object[rowMeta.size()]; + for (int i = 0; i < rowMeta.size(); i++) { + Object hopValue = hopRow == null || i >= hopRow.length ? null : hopRow[i]; + values[i] = toSparkValue(rowMeta.getValueMeta(i), hopValue); + } + return RowFactory.create(values); + } + + /** + * Spark row with a leading target-branch tag column ({@link HopSparkUtil#TARGET_TAG_COLUMN}) for + * multi-output transforms (Filter Rows, Switch/Case). + */ + public static Row toTaggedSparkRow(String targetTag, IRowMeta rowMeta, Object[] hopRow) + throws HopException { + Object[] values = new Object[rowMeta.size() + 1]; + values[0] = targetTag != null ? targetTag : HopSparkUtil.MAIN_TARGET_TAG; + for (int i = 0; i < rowMeta.size(); i++) { + Object hopValue = hopRow == null || i >= hopRow.length ? null : hopRow[i]; + values[i + 1] = toSparkValue(rowMeta.getValueMeta(i), hopValue); + } + return RowFactory.create(values); + } + + /** Schema for {@link #toTaggedSparkRow}: tag string + payload fields. */ + public static StructType toTaggedStructType(IRowMeta rowMeta) throws HopException { + StructType payload = toStructType(rowMeta); + StructField tagField = + new StructField( + HopSparkUtil.TARGET_TAG_COLUMN, DataTypes.StringType, false, Metadata.empty()); + StructField[] fields = new StructField[payload.size() + 1]; + fields[0] = tagField; + for (int i = 0; i < payload.size(); i++) { + fields[i + 1] = payload.fields()[i]; + } + return new StructType(fields); + } + + public static Object[] toHopRow(IRowMeta rowMeta, Row sparkRow) throws HopException { + Object[] hopRow = new Object[rowMeta.size()]; + if (sparkRow == null) { + return hopRow; + } + // Tolerate short Spark rows (schema drift) — pad remaining Hop fields with null. + int available = Math.min(rowMeta.size(), sparkRow.length()); + for (int i = 0; i < available; i++) { + hopRow[i] = + toHopValue(rowMeta.getValueMeta(i), sparkRow.isNullAt(i) ? null : sparkRow.get(i)); + } + return hopRow; + } + + static Object toSparkValue(IValueMeta valueMeta, Object hopValue) throws HopException { + if (hopValue == null) { + return null; + } + try { + return switch (valueMeta.getType()) { + case IValueMeta.TYPE_STRING, IValueMeta.TYPE_INET -> valueMeta.getString(hopValue); + case IValueMeta.TYPE_INTEGER -> valueMeta.getInteger(hopValue); + case IValueMeta.TYPE_NUMBER -> valueMeta.getNumber(hopValue); + case IValueMeta.TYPE_BIGNUMBER -> valueMeta.getBigNumber(hopValue); + case IValueMeta.TYPE_BOOLEAN -> valueMeta.getBoolean(hopValue); + case IValueMeta.TYPE_DATE, IValueMeta.TYPE_TIMESTAMP -> { + Date date = valueMeta.getDate(hopValue); + yield date == null ? null : new Timestamp(date.getTime()); + } + case IValueMeta.TYPE_BINARY -> valueMeta.getBinary(hopValue); + default -> valueMeta.getString(hopValue); + }; + } catch (Exception e) { + throw new HopException( + "Error converting Hop value to Spark for field '" + valueMeta.getName() + "'", e); + } + } + + static Object toHopValue(IValueMeta valueMeta, Object sparkValue) throws HopException { + if (sparkValue == null) { + return null; + } + try { + return switch (valueMeta.getType()) { + case IValueMeta.TYPE_STRING, IValueMeta.TYPE_INET -> sparkValue.toString(); + case IValueMeta.TYPE_INTEGER -> { + if (sparkValue instanceof Number number) { + yield number.longValue(); + } + yield Long.parseLong(sparkValue.toString()); + } + case IValueMeta.TYPE_NUMBER -> { + if (sparkValue instanceof Number number) { + yield number.doubleValue(); + } + yield Double.parseDouble(sparkValue.toString()); + } + case IValueMeta.TYPE_BIGNUMBER -> { + if (sparkValue instanceof BigDecimal bigDecimal) { + yield bigDecimal; + } + if (sparkValue instanceof Number number) { + yield BigDecimal.valueOf(number.doubleValue()); + } + yield new BigDecimal(sparkValue.toString()); + } + case IValueMeta.TYPE_BOOLEAN -> { + if (sparkValue instanceof Boolean bool) { + yield bool; + } + yield Boolean.parseBoolean(sparkValue.toString()); + } + case IValueMeta.TYPE_DATE, IValueMeta.TYPE_TIMESTAMP -> { + if (sparkValue instanceof Timestamp timestamp) { + yield new Date(timestamp.getTime()); + } + if (sparkValue instanceof Date date) { + yield date; + } + if (sparkValue instanceof java.time.Instant instant) { + yield Date.from(instant); + } + yield sparkValue; + } + case IValueMeta.TYPE_BINARY -> { + if (sparkValue instanceof byte[] bytes) { + yield bytes; + } + if (sparkValue instanceof ByteBuffer buffer) { + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + yield bytes; + } + yield sparkValue; + } + default -> sparkValue.toString(); + }; + } catch (Exception e) { + throw new HopException( + "Error converting Spark value to Hop for field '" + valueMeta.getName() + "'", e); + } + } + + /** Whether a Spark DataType is a decimal (for tests). */ + public static boolean isDecimal(DataType dataType) { + return dataType instanceof DecimalType; + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/HopSparkUtil.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/HopSparkUtil.java new file mode 100644 index 00000000000..c4d425a6f06 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/HopSparkUtil.java @@ -0,0 +1,81 @@ +/* + * 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.hop.spark.core; + +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.xml.XmlHandler; +import org.apache.hop.core.xml.XmlHandlerCache; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.transform.ITransformMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +public final class HopSparkUtil { + + private static final Object LOAD_LOCK = new Object(); + + /** + * Marker value for the main (non-targeted) output branch in tagged mapPartitions rows. Must not + * collide with a real transform name. + */ + public static final String MAIN_TARGET_TAG = "__main__"; + + /** Temporary column used to split multi-target mapPartitions output into Datasets. */ + public static final String TARGET_TAG_COLUMN = "_hop_target"; + + private HopSparkUtil() {} + + /** + * Dataset-map key for a named target stream (matches Beam {@code HopBeamUtil.createTargetTupleId} + * string format; no dependency on the Beam plugin). + */ + public static String createTargetTupleId(String transformName, String targetTransformName) { + return transformName + " - TARGET - " + targetTransformName; + } + + public static void loadTransformMetadataFromXml( + String transformName, + ITransformMeta iTransformMeta, + String transformXml, + IHopMetadataProvider metadataProvider) + throws HopException { + synchronized (LOAD_LOCK) { + Document transformDocument = XmlHandler.loadXmlString(transformXml); + if (transformDocument == null) { + throw new HopException("Unable to load transform XML document from : " + transformXml); + } + Node transformNode = XmlHandler.getSubNode(transformDocument, TransformMeta.XML_TAG); + if (transformNode == null) { + throw new HopException( + "Unable to find XML tag " + TransformMeta.XML_TAG + " from : " + transformXml); + } + try { + iTransformMeta.loadXml(transformNode, metadataProvider); + } catch (Exception e) { + throw new HopException( + "There was an error loading transform metadata information (loadXml) for transform '" + + transformName + + "'", + e); + } finally { + XmlHandlerCache.getInstance().clear(); + } + } + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkExecutionDataAccumulator.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkExecutionDataAccumulator.java new file mode 100644 index 00000000000..8b279dbc5d5 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkExecutionDataAccumulator.java @@ -0,0 +1,132 @@ +/* + * 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.hop.spark.core; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.apache.spark.util.AccumulatorV2; + +/** + * Collects JSON-serialized {@code ExecutionData} samples from Spark executors and merges them on + * the driver. Keyed by owner id (transform copy log channel id) so the latest flush per copy wins. + * + *

Caching execution info locations keep a single parent {@code CacheEntry} in the driver + * process; executor-side {@code registerData} cannot reliably attach samples to that entry. + * Shipping samples through this accumulator lets the driver register them where the parent entry + * already exists. + */ +public class SparkExecutionDataAccumulator extends AccumulatorV2> { + + private static final long serialVersionUID = 1L; + + /** Delimiter between ownerId and JSON payload in accumulator elements. */ + private static final char SEP = '\u0001'; + + private final Map byOwnerId; + + public SparkExecutionDataAccumulator() { + this.byOwnerId = new HashMap<>(); + } + + private SparkExecutionDataAccumulator(Map byOwnerId) { + this.byOwnerId = byOwnerId; + } + + /** + * Pack owner id + JSON for {@link #add(String)}. Prefer {@link #addSample(String, String)} from + * Hop code. + */ + public static String pack(String ownerId, String executionDataJson) { + if (ownerId == null) { + ownerId = ""; + } + if (executionDataJson == null) { + executionDataJson = ""; + } + return ownerId + SEP + executionDataJson; + } + + public void addSample(String ownerId, String executionDataJson) { + add(pack(ownerId, executionDataJson)); + } + + @Override + public boolean isZero() { + synchronized (byOwnerId) { + return byOwnerId.isEmpty(); + } + } + + @Override + public AccumulatorV2> copy() { + synchronized (byOwnerId) { + return new SparkExecutionDataAccumulator(new HashMap<>(byOwnerId)); + } + } + + @Override + public void reset() { + synchronized (byOwnerId) { + byOwnerId.clear(); + } + } + + @Override + public void add(String v) { + if (v == null || v.isEmpty()) { + return; + } + int idx = v.indexOf(SEP); + if (idx < 0) { + return; + } + String ownerId = v.substring(0, idx); + String json = v.substring(idx + 1); + if (ownerId.isEmpty() || json.isEmpty()) { + return; + } + synchronized (byOwnerId) { + byOwnerId.put(ownerId, json); + } + } + + @Override + public void merge(AccumulatorV2> other) { + if (other == null) { + return; + } + Map otherMap = other.value(); + if (otherMap == null || otherMap.isEmpty()) { + return; + } + synchronized (byOwnerId) { + byOwnerId.putAll(otherMap); + } + } + + @Override + public Map value() { + synchronized (byOwnerId) { + if (byOwnerId.isEmpty()) { + return Collections.emptyMap(); + } + return Collections.unmodifiableMap(new HashMap<>(byOwnerId)); + } + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkHop.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkHop.java new file mode 100644 index 00000000000..61d852b3739 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkHop.java @@ -0,0 +1,43 @@ +/* + * 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.hop.spark.core; + +import org.apache.hop.core.Const; +import org.apache.hop.core.HopEnvironment; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.plugins.PluginRegistry; +import org.apache.hop.core.xml.XmlHandlerCache; + +/** Hop initialization helpers for Spark executors (mirrors BeamHop). */ +public final class SparkHop { + + private SparkHop() {} + + public static boolean isInitialized() { + return HopEnvironment.isInitialized(); + } + + public static void init() throws HopException { + synchronized (PluginRegistry.getInstance()) { + // Don't create hop config files on every executor JVM + System.setProperty(Const.HOP_AUTO_CREATE_CONFIG, "N"); + HopEnvironment.init(); + XmlHandlerCache.getInstance(); + } + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkInfoStreamSupport.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkInfoStreamSupport.java new file mode 100644 index 00000000000..17802e0e5ab --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkInfoStreamSupport.java @@ -0,0 +1,99 @@ +/* + * 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.hop.spark.core; + +import java.util.ArrayList; +import java.util.List; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.row.IRowMeta; +import org.apache.spark.api.java.JavaSparkContext; +import org.apache.spark.broadcast.Broadcast; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; + +/** + * Collects an info/side Dataset into Hop rows and broadcasts it to executors (Beam {@code + * View.asList} analogue). + * + *

Info streams must fit in memory on the driver and on each executor that receives the + * broadcast. + */ +public final class SparkInfoStreamSupport { + + /** Soft guard: fail fast if an info stream is unexpectedly huge. 0 = unlimited. */ + public static final int DEFAULT_MAX_INFO_ROWS = 5_000_000; + + private SparkInfoStreamSupport() {} + + /** + * Collect {@code infoDataset} as Hop {@code Object[]} rows and broadcast them. + * + * @param spark active session + * @param infoDataset source Dataset (already computed in the transform graph) + * @param rowMeta Hop row metadata matching the Dataset columns + * @param transformName name of the info source (for error messages) + * @param maxRows maximum rows to collect; use {@link #DEFAULT_MAX_INFO_ROWS} or {@literal <=0} + * for unlimited + * @return broadcast of Hop rows (empty list if Dataset has no rows) + */ + public static Broadcast> broadcastInfoRows( + SparkSession spark, + Dataset infoDataset, + IRowMeta rowMeta, + String transformName, + int maxRows) + throws HopException { + if (spark == null || infoDataset == null) { + throw new HopException( + "Cannot broadcast info stream for transform '" + + transformName + + "': null session/dataset"); + } + if (rowMeta == null) { + throw new HopException( + "Cannot broadcast info stream for transform '" + transformName + "': null row meta"); + } + + List sparkRows = infoDataset.collectAsList(); + if (maxRows > 0 && sparkRows.size() > maxRows) { + throw new HopException( + "Info stream from transform '" + + transformName + + "' has " + + sparkRows.size() + + " rows which exceeds the maximum allowed (" + + maxRows + + "). Reduce the lookup size or raise the limit."); + } + + List hopRows = new ArrayList<>(sparkRows.size()); + for (Row sparkRow : sparkRows) { + hopRows.add(HopSparkRowConverter.toHopRow(rowMeta, sparkRow)); + } + + JavaSparkContext jsc = JavaSparkContext.fromSparkContext(spark.sparkContext()); + return jsc.broadcast(hopRows); + } + + public static Broadcast> broadcastInfoRows( + SparkSession spark, Dataset infoDataset, IRowMeta rowMeta, String transformName) + throws HopException { + return broadcastInfoRows(spark, infoDataset, rowMeta, transformName, DEFAULT_MAX_INFO_ROWS); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkNativeMetrics.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkNativeMetrics.java new file mode 100644 index 00000000000..0c19f06a8fa --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkNativeMetrics.java @@ -0,0 +1,210 @@ +/* + * 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.hop.spark.core; + +import java.io.Serializable; +import java.net.InetAddress; +import java.util.Iterator; +import java.util.Objects; +import org.apache.spark.TaskContext; +import org.apache.spark.api.java.JavaRDD; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.types.StructType; + +/** + * Instruments native Spark {@link Dataset} stages so row flow is visible in Hop {@code + * EngineMetrics}. Inserts a pass-through {@code mapPartitions} that reports absolute counters into + * the same {@link SparkTransformMetricsAccumulator} used by {@link HopMapPartitionsFn}. + * + *

Counters are only updated when the lineage is materialized (an action). Per-partition {@code + * copyNr} matches {@link TaskContext#partitionId()}. + */ +public final class SparkNativeMetrics { + + /** How counters are attributed for a native Dataset stage. */ + public enum Role { + /** Source / file input: physical input + rows produced. */ + INPUT, + /** Sink / file output: rows consumed + physical output. */ + OUTPUT, + /** Intermediate Dataset op: rows in and out of the stage (output cardinality). */ + TRANSFORM + } + + private static final int ROW_INTERVAL = 1000; + private static final long TIME_INTERVAL_MS = 1000L; + + private SparkNativeMetrics() {} + + /** + * Wrap {@code dataset} so that when it is computed, each partition reports metrics for {@code + * transformName}. Returns {@code dataset} unchanged when accumulator or name is null. + */ + public static Dataset track( + Dataset dataset, + String transformName, + SparkTransformMetricsAccumulator accumulator, + Role role) { + if (dataset == null + || accumulator == null + || transformName == null + || transformName.isEmpty()) { + return dataset; + } + Role effectiveRole = role != null ? role : Role.TRANSFORM; + StructType schema = dataset.schema(); + JavaRDD tracked = + dataset + .toJavaRDD() + .mapPartitions( + iterator -> + new CountingIterator(iterator, transformName, accumulator, effectiveRole), + true); + return dataset.sparkSession().createDataFrame(tracked, schema); + } + + static final class CountingIterator implements Iterator, Serializable { + private static final long serialVersionUID = 1L; + + private final Iterator source; + private final String transformName; + private final SparkTransformMetricsAccumulator accumulator; + private final Role role; + private final int copyNr; + private final String host; + + private long count; + private long partitionStartMs; + private long lastPublishMs; + private boolean finishedPublished; + private boolean startedPublished; + + CountingIterator( + Iterator source, + String transformName, + SparkTransformMetricsAccumulator accumulator, + Role role) { + this.source = Objects.requireNonNull(source, "source"); + this.transformName = transformName; + this.accumulator = accumulator; + this.role = role; + this.copyNr = partitionId(); + this.host = localHost(); + } + + @Override + public boolean hasNext() { + ensureStarted(); + if (source.hasNext()) { + return true; + } + publishFinished(); + return false; + } + + @Override + public Row next() { + ensureStarted(); + Row row = source.next(); + count++; + if (shouldPublishProgress()) { + publish(true, false); + } + return row; + } + + private void ensureStarted() { + if (!startedPublished) { + startedPublished = true; + partitionStartMs = System.currentTimeMillis(); + lastPublishMs = partitionStartMs; + publish(true, false); + } + } + + private boolean shouldPublishProgress() { + if (count > 0 && count % ROW_INTERVAL == 0) { + return true; + } + long now = System.currentTimeMillis(); + if (now - lastPublishMs >= TIME_INTERVAL_MS) { + lastPublishMs = now; + return true; + } + return false; + } + + private void publishFinished() { + if (!finishedPublished) { + finishedPublished = true; + publish(false, true); + } + } + + private void publish(boolean running, boolean finished) { + long read = 0; + long written = 0; + long input = 0; + long output = 0; + switch (role) { + case INPUT: + input = count; + written = count; + break; + case OUTPUT: + read = count; + output = count; + break; + case TRANSFORM: + default: + read = count; + written = count; + break; + } + long endTimeMs = finished ? System.currentTimeMillis() : 0L; + accumulator.add( + new SparkTransformMetricSlice( + transformName, + copyNr, + host, + read, + written, + input, + output, + 0, + running, + finished, + partitionStartMs, + endTimeMs)); + } + + private static int partitionId() { + TaskContext ctx = TaskContext.get(); + return ctx != null ? ctx.partitionId() : 0; + } + + private static String localHost() { + try { + return InetAddress.getLocalHost().getHostName(); + } catch (Exception e) { + return null; + } + } + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkRowHandler.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkRowHandler.java new file mode 100644 index 00000000000..dff5a7a3932 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkRowHandler.java @@ -0,0 +1,128 @@ +/* + * 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.hop.spark.core; + +import java.util.List; +import org.apache.hop.core.IRowSet; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.exception.HopTransformException; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.pipeline.transform.BaseTransform; +import org.apache.hop.pipeline.transform.IRowHandler; +import org.apache.hop.pipeline.transform.IRowListener; + +/** + * Non-blocking row I/O for single-threaded mini-pipelines on Spark executors (same idea as Beam's + * {@code BeamRowHandler}). + * + *

The default {@link BaseTransform} row handler busy-waits when an input hop is empty and not + * yet done. That deadlocks Stream Lookup: after {@code readLookupValues()} it calls {@code + * getRow()} for the main stream while the single-threaded executor has not yet (or cannot) produce + * the next main row on another thread. + */ +public class SparkRowHandler implements IRowHandler { + + private final BaseTransform transform; + + public SparkRowHandler(BaseTransform transform) { + this.transform = transform; + } + + /** + * Prefer live rowsets from the transform (not a snapshot at construction): hop wiring and {@code + * addRowProducer} can still change after the handler is installed. + */ + private IRowSet firstInput() { + List inputRowSets = transform.getInputRowSets(); + return inputRowSets == null || inputRowSets.isEmpty() ? null : inputRowSets.get(0); + } + + private IRowSet firstOutput() { + List outputRowSets = transform.getOutputRowSets(); + return outputRowSets == null || outputRowSets.isEmpty() ? null : outputRowSets.get(0); + } + + @Override + public Object[] getRow() throws HopException { + IRowSet inputRowSet = firstInput(); + if (inputRowSet == null) { + return null; + } + Object[] row = inputRowSet.getRow(); + if (row != null) { + transform.incrementLinesRead(); + transform.setInputRowMeta(inputRowSet.getRowMeta()); + List rowListeners = transform.getRowListeners(); + for (IRowListener rowListener : rowListeners) { + rowListener.rowReadEvent(inputRowSet.getRowMeta(), row); + } + } + return row; + } + + @Override + public void putRow(IRowMeta rowMeta, Object[] row) throws HopTransformException { + List rowListeners = transform.getRowListeners(); + for (IRowListener rowListener : rowListeners) { + rowListener.rowWrittenEvent(rowMeta, row); + } + IRowSet outputRowSet = firstOutput(); + if (outputRowSet != null) { + outputRowSet.putRow(rowMeta, row); + } + // Always count written (including leaf transforms with no output hop) + transform.incrementLinesWritten(); + } + + @Override + public void putError( + IRowMeta rowMeta, + Object[] row, + long nrErrors, + String errorDescriptions, + String fieldNames, + String errorCodes) + throws HopTransformException { + transform.handlePutError( + transform, rowMeta, row, nrErrors, errorDescriptions, fieldNames, errorCodes); + } + + @Override + public void putRowTo(IRowMeta rowMeta, Object[] row, IRowSet rowSet) + throws HopTransformException { + List rowListeners = transform.getRowListeners(); + for (IRowListener listener : rowListeners) { + listener.rowWrittenEvent(rowMeta, row); + } + rowSet.putRow(rowMeta, row); + transform.incrementLinesWritten(); + } + + @Override + public Object[] getRowFrom(IRowSet rowSet) throws HopTransformException { + Object[] row = rowSet.getRow(); + if (row != null) { + transform.incrementLinesRead(); + List rowListeners = transform.getRowListeners(); + for (IRowListener listener : rowListeners) { + listener.rowReadEvent(rowSet.getRowMeta(), row); + } + } + return row; + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkTransformMetricSlice.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkTransformMetricSlice.java new file mode 100644 index 00000000000..d62f734dc7e --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkTransformMetricSlice.java @@ -0,0 +1,232 @@ +/* + * 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.hop.spark.core; + +import java.io.Serializable; +import java.util.Objects; + +/** + * Snapshot of Hop transform counters for one Spark partition (engine component copy). + * + *

Counters are absolute values from the mini-pipeline / native tracker on that partition. + * Merging multiple snapshots for the same key takes the maximum of each counter (progress updates), + * not a sum. Timing uses earliest start and latest end (epoch millis). + */ +public class SparkTransformMetricSlice implements Serializable { + private static final long serialVersionUID = 2L; + + private final String transformName; + private final int copyNr; + private final String host; + private final long linesRead; + private final long linesWritten; + private final long linesInput; + private final long linesOutput; + private final long errors; + private final boolean running; + private final boolean finished; + + /** Wall-clock start of this partition's work (epoch ms), or 0 if unknown. */ + private final long startTimeMs; + + /** + * Wall-clock end of this partition's work (epoch ms). 0 while still running so the GUI can treat + * duration as "live" until finish. + */ + private final long endTimeMs; + + public SparkTransformMetricSlice( + String transformName, + int copyNr, + String host, + long linesRead, + long linesWritten, + long linesInput, + long linesOutput, + long errors, + boolean running, + boolean finished, + long startTimeMs, + long endTimeMs) { + this.transformName = transformName; + this.copyNr = copyNr; + this.host = host; + this.linesRead = linesRead; + this.linesWritten = linesWritten; + this.linesInput = linesInput; + this.linesOutput = linesOutput; + this.errors = errors; + this.running = running; + this.finished = finished; + this.startTimeMs = startTimeMs; + this.endTimeMs = endTimeMs; + } + + /** Map key for merge-by-partition. */ + public String key() { + return transformName + '\0' + copyNr; + } + + /** + * Merge two absolute snapshots for the same partition: max counters; min start; max end when + * finished; finished wins over running. + */ + public SparkTransformMetricSlice mergeWith(SparkTransformMetricSlice other) { + if (other == null) { + return this; + } + boolean mergedFinished = this.finished || other.finished; + boolean mergedRunning = !mergedFinished && (this.running || other.running); + String mergedHost = this.host != null ? this.host : other.host; + long mergedStart = minPositive(this.startTimeMs, other.startTimeMs); + long mergedEnd; + if (mergedFinished) { + mergedEnd = Math.max(this.endTimeMs, other.endTimeMs); + // If finish was reported without a clock stamp, fall back to the later of known ends/starts + if (mergedEnd <= 0) { + mergedEnd = Math.max(this.endTimeMs, other.endTimeMs); + } + } else { + // Still running: keep end unset so UI duration keeps ticking + mergedEnd = 0L; + } + return new SparkTransformMetricSlice( + this.transformName, + this.copyNr, + mergedHost, + Math.max(this.linesRead, other.linesRead), + Math.max(this.linesWritten, other.linesWritten), + Math.max(this.linesInput, other.linesInput), + Math.max(this.linesOutput, other.linesOutput), + Math.max(this.errors, other.errors), + mergedRunning, + mergedFinished, + mergedStart, + mergedEnd); + } + + private static long minPositive(long a, long b) { + if (a <= 0) { + return b; + } + if (b <= 0) { + return a; + } + return Math.min(a, b); + } + + /** + * Duration in milliseconds for this slice. While running (no end), uses wall clock from start to + * now. Returns 0 when start is unknown. + */ + public long durationMs() { + if (startTimeMs <= 0) { + return 0L; + } + long end = endTimeMs > 0 ? endTimeMs : System.currentTimeMillis(); + return Math.max(0L, end - startTimeMs); + } + + public String getTransformName() { + return transformName; + } + + public int getCopyNr() { + return copyNr; + } + + public String getHost() { + return host; + } + + public long getLinesRead() { + return linesRead; + } + + public long getLinesWritten() { + return linesWritten; + } + + public long getLinesInput() { + return linesInput; + } + + public long getLinesOutput() { + return linesOutput; + } + + public long getErrors() { + return errors; + } + + public boolean isRunning() { + return running; + } + + public boolean isFinished() { + return finished; + } + + public long getStartTimeMs() { + return startTimeMs; + } + + public long getEndTimeMs() { + return endTimeMs; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SparkTransformMetricSlice that = (SparkTransformMetricSlice) o; + return copyNr == that.copyNr + && linesRead == that.linesRead + && linesWritten == that.linesWritten + && linesInput == that.linesInput + && linesOutput == that.linesOutput + && errors == that.errors + && running == that.running + && finished == that.finished + && startTimeMs == that.startTimeMs + && endTimeMs == that.endTimeMs + && Objects.equals(transformName, that.transformName) + && Objects.equals(host, that.host); + } + + @Override + public int hashCode() { + return Objects.hash( + transformName, + copyNr, + host, + linesRead, + linesWritten, + linesInput, + linesOutput, + errors, + running, + finished, + startTimeMs, + endTimeMs); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkTransformMetricsAccumulator.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkTransformMetricsAccumulator.java new file mode 100644 index 00000000000..9a340925322 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkTransformMetricsAccumulator.java @@ -0,0 +1,106 @@ +/* + * 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.hop.spark.core; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.apache.spark.util.AccumulatorV2; + +/** + * Merging Spark accumulator of per-partition Hop transform metric slices. Registered on the driver + * and referenced from {@link HopMapPartitionsFn} closures so executors can report progress. + */ +public class SparkTransformMetricsAccumulator + extends AccumulatorV2> { + + private static final long serialVersionUID = 1L; + + private final Map map; + + public SparkTransformMetricsAccumulator() { + this.map = new HashMap<>(); + } + + private SparkTransformMetricsAccumulator(Map map) { + this.map = map; + } + + @Override + public boolean isZero() { + synchronized (map) { + return map.isEmpty(); + } + } + + @Override + public AccumulatorV2> copy() { + synchronized (map) { + return new SparkTransformMetricsAccumulator(new HashMap<>(map)); + } + } + + @Override + public void reset() { + synchronized (map) { + map.clear(); + } + } + + @Override + public void add(SparkTransformMetricSlice v) { + if (v == null || v.getTransformName() == null) { + return; + } + synchronized (map) { + String key = v.key(); + SparkTransformMetricSlice existing = map.get(key); + map.put(key, existing == null ? v : existing.mergeWith(v)); + } + } + + @Override + public void merge( + AccumulatorV2> other) { + if (other == null) { + return; + } + Map otherMap = other.value(); + if (otherMap == null || otherMap.isEmpty()) { + return; + } + synchronized (map) { + for (Map.Entry entry : otherMap.entrySet()) { + SparkTransformMetricSlice existing = map.get(entry.getKey()); + map.put( + entry.getKey(), + existing == null ? entry.getValue() : existing.mergeWith(entry.getValue())); + } + } + } + + @Override + public Map value() { + synchronized (map) { + if (map.isEmpty()) { + return Collections.emptyMap(); + } + return Collections.unmodifiableMap(new HashMap<>(map)); + } + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkVariableValue.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkVariableValue.java new file mode 100644 index 00000000000..c34c93b1fa7 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/core/SparkVariableValue.java @@ -0,0 +1,41 @@ +/* + * 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.hop.spark.core; + +import java.io.Serializable; + +/** Serializable variable name/value pair for Spark executors. */ +public class SparkVariableValue implements Serializable { + private static final long serialVersionUID = 1L; + + private final String variable; + private final String value; + + public SparkVariableValue(String variable, String value) { + this.variable = variable; + this.value = value; + } + + public String getVariable() { + return variable; + } + + public String getValue() { + return value; + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/engines/ISparkPipelineEngineRunConfiguration.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/engines/ISparkPipelineEngineRunConfiguration.java new file mode 100644 index 00000000000..19c9a241cb1 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/engines/ISparkPipelineEngineRunConfiguration.java @@ -0,0 +1,54 @@ +/* + * 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.hop.spark.engines; + +import org.apache.hop.pipeline.config.IPipelineEngineRunConfiguration; + +public interface ISparkPipelineEngineRunConfiguration extends IPipelineEngineRunConfiguration { + + String getSparkMaster(); + + String getSparkAppName(); + + String getFatJar(); + + String getSparkConfigs(); + + String getDriverMemory(); + + String getExecutorMemory(); + + String getExecutorCores(); + + String getTempLocation(); + + String getPluginsToStage(); + + /** + * Optional multi-line path scheme map for Spark Dataset / Lake PATH I/O ({@code from=to} per + * line, e.g. {@code s3=s3a}). Rewrites only the URI scheme before {@code load}/{@code save}; + * classic Hop VFS paths are unaffected. + */ + String getPathSchemeMap(); + + /** + * Default execution mode for generic (mapPartitions) Hop transforms: {@code DISTRIBUTED} or + * {@code DRIVER_ONLY}. Per-transform overrides can force either mode via transform attributes. + */ + String getGenericTransformRunMode(); +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/engines/SparkPipelineEngine.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/engines/SparkPipelineEngine.java new file mode 100644 index 00000000000..2b4c56ef52e --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/engines/SparkPipelineEngine.java @@ -0,0 +1,1841 @@ +/* + * 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.hop.spark.engines; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.Const; +import org.apache.hop.core.IRowSet; +import org.apache.hop.core.Result; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.extension.ExtensionPointHandler; +import org.apache.hop.core.extension.HopExtensionPoint; +import org.apache.hop.core.json.HopJson; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.logging.ILoggingObject; +import org.apache.hop.core.logging.LogChannel; +import org.apache.hop.core.logging.LogLevel; +import org.apache.hop.core.logging.LoggingObjectType; +import org.apache.hop.core.parameters.DuplicateParamException; +import org.apache.hop.core.parameters.INamedParameterDefinitions; +import org.apache.hop.core.parameters.INamedParameters; +import org.apache.hop.core.parameters.NamedParameters; +import org.apache.hop.core.parameters.UnknownParamException; +import org.apache.hop.core.plugins.EngineCompatibility; +import org.apache.hop.core.plugins.IPlugin; +import org.apache.hop.core.util.ExecutorUtil; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.execution.Execution; +import org.apache.hop.execution.ExecutionBuilder; +import org.apache.hop.execution.ExecutionData; +import org.apache.hop.execution.ExecutionDataSetMeta; +import org.apache.hop.execution.ExecutionInfoLocation; +import org.apache.hop.execution.ExecutionState; +import org.apache.hop.execution.ExecutionStateBuilder; +import org.apache.hop.execution.ExecutionType; +import org.apache.hop.execution.IExecutionInfoLocation; +import org.apache.hop.execution.sampler.IExecutionDataSampler; +import org.apache.hop.execution.sampler.IExecutionDataSamplerStore; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.IExecutionFinishedListener; +import org.apache.hop.pipeline.IExecutionStartedListener; +import org.apache.hop.pipeline.IExecutionStoppedListener; +import org.apache.hop.pipeline.Pipeline; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.config.IPipelineEngineRunConfiguration; +import org.apache.hop.pipeline.config.PipelineRunConfiguration; +import org.apache.hop.pipeline.engine.EngineComponent; +import org.apache.hop.pipeline.engine.EngineComponent.ComponentExecutionStatus; +import org.apache.hop.pipeline.engine.EngineMetrics; +import org.apache.hop.pipeline.engine.IEngineComponent; +import org.apache.hop.pipeline.engine.IEngineMetric; +import org.apache.hop.pipeline.engine.IPipelineComponentRowsReceived; +import org.apache.hop.pipeline.engine.IPipelineEngine; +import org.apache.hop.pipeline.engine.PipelineEngineCapabilities; +import org.apache.hop.pipeline.engine.PipelineEnginePlugin; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.core.SparkExecutionDataAccumulator; +import org.apache.hop.spark.core.SparkTransformMetricSlice; +import org.apache.hop.spark.core.SparkTransformMetricsAccumulator; +import org.apache.hop.spark.execution.SparkTransformExecutionSampling; +import org.apache.hop.spark.pipeline.HopPipelineMetaToSparkConverter; +import org.apache.hop.spark.pkg.SparkProjectPackage; +import org.apache.hop.spark.table.LakeSessionPlan; +import org.apache.hop.spark.util.SparkConst; +import org.apache.hop.workflow.WorkflowMeta; +import org.apache.hop.workflow.engine.IWorkflowEngine; +import org.apache.spark.SparkConf; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; +import scala.Option; + +/** + * Native Apache Spark 4.x pipeline engine. Converts Hop pipelines to Spark Dataset graphs and + * executes Hop transforms via mapPartitions mini-pipelines. + * + * @see issue #7486 + */ +@PipelineEnginePlugin( + id = SparkConst.PLUGIN_ID, + name = SparkConst.PLUGIN_NAME, + description = + "Executes batch Hop pipelines natively on Apache Spark 4.x (mapPartitions + Dataset API)") +@Getter +@Setter +public class SparkPipelineEngine extends Variables implements IPipelineEngine { + + private final PipelineEngineCapabilities engineCapabilities = + new SparkPipelineEngineCapabilities(); + + private PipelineMeta pipelineMeta; + private String pluginId = SparkConst.PLUGIN_ID; + private PipelineRunConfiguration pipelineRunConfiguration; + private boolean preparing; + private boolean readyToStart; + private boolean running; + private boolean finished; + private boolean stopped; + private boolean paused; + private boolean hasHaltedComponents; + private boolean preview; + private int errors; + private IHopMetadataProvider metadataProvider; + private ILogChannel logChannel = LogChannel.GENERAL; + private String containerId; + private EngineMetrics engineMetrics = new EngineMetrics(); + private Result previousResult; + + private ILoggingObject parent; + private IPipelineEngine parentPipeline; + private IWorkflowEngine parentWorkflow; + private LogLevel logLevel; + + private Date executionStartDate; + private Date executionEndDate; + + private final List>> + executionStartedListeners = Collections.synchronizedList(new ArrayList<>()); + private final List>> + executionFinishedListeners = Collections.synchronizedList(new ArrayList<>()); + private final List>> + executionStoppedListeners = Collections.synchronizedList(new ArrayList<>()); + + private final Map activeSubPipelines = new HashMap<>(); + private final Map> activeSubWorkflows = new HashMap<>(); + private final Map extensionDataMap = Collections.synchronizedMap(new HashMap<>()); + + private final INamedParameters namedParams = new NamedParameters(); + + private String statusDescription = Pipeline.STRING_WAITING; + private ComponentExecutionStatus status = ComponentExecutionStatus.STATUS_EMPTY; + + private HopPipelineMetaToSparkConverter converter; + private ISparkPipelineEngineRunConfiguration sparkEngineRunConfiguration; + private SparkSession sparkSession; + + /** + * True when this engine created the {@link SparkSession} (and may stop it). False when reusing an + * active/default session or nesting under a parent Spark engine — never stop/cancel the shared + * context in that case. + */ + private boolean sessionOwnedByThisEngine; + + private Dataset resultDataset; + private Thread sparkThread; + private SparkTransformMetricsAccumulator metricsAccumulator; + private SparkExecutionDataAccumulator sampleDataAccumulator; + private Timer metricsRefreshTimer; + + /** Execution information location from the run configuration (optional). */ + private ExecutionInfoLocation executionInfoLocation; + + private Timer executionInfoTimer; + private volatile boolean executionInfoClosed; + + /** Stable log channel IDs per transform name + copy for execution info / GUI. */ + private final Map componentLogChannelIds = new ConcurrentHashMap<>(); + + private final List> dataSamplers = + Collections.synchronizedList(new ArrayList<>()); + + public SparkPipelineEngine() { + super(); + } + + public SparkPipelineEngine( + PipelineMeta pipelineMeta, ILoggingObject parent, IVariables variables) { + this(); + this.pipelineMeta = pipelineMeta; + setParent(parent); + initializeFrom(variables); + copyParametersFromDefinitions(pipelineMeta); + activateParameters(this); + } + + @Override + public IPipelineEngineRunConfiguration createDefaultPipelineEngineRunConfiguration() { + return new SparkPipelineRunConfiguration(); + } + + public void validatePipelineRunConfigurationClass( + IPipelineEngineRunConfiguration engineRunConfiguration) throws HopException { + if (!(engineRunConfiguration instanceof SparkPipelineRunConfiguration)) { + throw new HopException( + "A native Spark pipeline engine needs a Spark run configuration, not of class " + + engineRunConfiguration.getClass().getName()); + } + } + + @Override + public void prepareExecution() throws HopException { + try { + executionStartDate = new Date(); + status = ComponentExecutionStatus.STATUS_INIT; + statusDescription = Pipeline.STRING_INITIALIZING; + setPreparing(true); + + IPipelineEngineRunConfiguration engineRunConfiguration = + pipelineRunConfiguration.getEngineRunConfiguration(); + validatePipelineRunConfigurationClass(engineRunConfiguration); + sparkEngineRunConfiguration = (ISparkPipelineEngineRunConfiguration) engineRunConfiguration; + + if (metadataProvider == null) { + throw new HopException("The Spark pipeline engine did not receive a metadata provider"); + } + + // Force a new log channel ID on every prepare/run (same as LocalPipelineEngine). Nested + // Pipeline Executor runs of the same child HPL would otherwise reuse one LoggingRegistry + // entry (same filename + parent transform) and overwrite a single execution-info file. + logChannel = new LogChannel(this, parent, false, true); + if (logLevel != null) { + logChannel.setLogLevel(logLevel); + } + + // Execution info location (optional): load + initialize for later register/update/close + lookupExecutionInformationLocation(); + + converter = + new HopPipelineMetaToSparkConverter( + this, + pipelineMeta, + metadataProvider, + pipelineRunConfiguration.getName(), + sparkEngineRunConfiguration); + converter.validatePipeline(); + + logChannel.logBasic( + "Prepared native Spark pipeline engine with run configuration '" + + pipelineRunConfiguration.getName() + + "' (master=" + + resolve(sparkEngineRunConfiguration.getSparkMaster()) + + ")"); + + setRunning(false); + setReadyToStart(true); + } catch (Exception e) { + setRunning(false); + setReadyToStart(false); + setStopped(true); + setErrors(getErrors() + 1); + setPaused(false); + setPreparing(false); + throw new HopException("Error preparing Spark pipeline", e); + } finally { + setPreparing(false); + } + } + + @Override + public void startThreads() throws HopException { + ClassLoader pluginCl = getClass().getClassLoader(); + ClassLoader previousCl = Thread.currentThread().getContextClassLoader(); + try { + // Spark 4 resolves classic.SparkSession via the context classloader / plugin CL + Thread.currentThread().setContextClassLoader(pluginCl); + + setRunning(true); + setReadyToStart(false); + status = ComponentExecutionStatus.STATUS_RUNNING; + statusDescription = Pipeline.STRING_RUNNING; + + sparkSession = createSparkSession(); + requireClassicSparkContext(sparkSession); + + // Register metrics + sample-data accumulators for mapPartitions transforms + metricsAccumulator = new SparkTransformMetricsAccumulator(); + sparkSession.sparkContext().register(metricsAccumulator, "hop-transform-metrics"); + converter.setMetricsAccumulator(metricsAccumulator); + sampleDataAccumulator = new SparkExecutionDataAccumulator(); + sparkSession.sparkContext().register(sampleDataAccumulator, "hop-execution-sample-data"); + converter.setSampleDataAccumulator(sampleDataAccumulator); + try { + converter.setExecutionSamplingContext( + getLogChannelId(), SparkTransformExecutionSampling.serializeDataSamplers(dataSamplers)); + } catch (HopException e) { + logChannel.logError("Error serializing execution data samplers (non-fatal)", e); + converter.setExecutionSamplingContext(getLogChannelId(), "[]"); + } + seedEngineMetricsComponents(); + + // Register at execution info location (if configured) and keep state updated + try { + registerPipelineExecutionInformation(); + startExecutionInfoTimer(); + } catch (HopException e) { + logChannel.logError("Error starting execution information tracking (non-fatal)", e); + } + + fireExecutionStartedListeners(); + ExtensionPointHandler.callExtensionPoint( + logChannel, this, HopExtensionPoint.PipelineStart.id, this); + + // Build + materialize Dataset graph on a worker thread so waitUntilFinished can poll + sparkThread = + new Thread( + () -> { + ClassLoader prev = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(pluginCl); + resultDataset = converter.createDataset(sparkSession); + // Materialize: count is a safe action when no dedicated sink handler wrote data + long rows = resultDataset.count(); + logChannel.logBasic("Spark pipeline finished, result row count=" + rows); + setErrors(0); + } catch (Throwable e) { + logChannel.logError("Error executing Spark pipeline", e); + setErrors(getErrors() + 1); + setStopped(true); + } finally { + Thread.currentThread().setContextClassLoader(prev); + setRunning(false); + setFinished(true); + executionEndDate = new Date(); + if (getErrors() == 0) { + status = ComponentExecutionStatus.STATUS_FINISHED; + statusDescription = Pipeline.STRING_FINISHED; + } else { + status = ComponentExecutionStatus.STATUS_STOPPED; + statusDescription = Pipeline.STRING_STOPPED; + } + ExecutorUtil.cleanup(metricsRefreshTimer); + try { + populateEngineMetrics(); + } catch (Exception emEx) { + logChannel.logError("Error populating final engine metrics", emEx); + } + try { + stopExecutionInfoTimer(); + } catch (Exception eiEx) { + logChannel.logError("Error finalizing execution information (non-fatal)", eiEx); + } + try { + fireExecutionFinishedListeners(); + } catch (HopException ex) { + logChannel.logError("Error firing finished listeners", ex); + } + } + }, + "hop-spark-pipeline"); + sparkThread.setContextClassLoader(pluginCl); + sparkThread.start(); + + // Live progress for the Hop Metrics grid (same cadence as Beam) + metricsRefreshTimer = new Timer("hop-spark-metrics-refresh", true); + metricsRefreshTimer.schedule( + new TimerTask() { + @Override + public void run() { + try { + if (isFinished() || isStopped()) { + ExecutorUtil.cleanup(metricsRefreshTimer); + return; + } + populateEngineMetrics(); + } catch (Throwable e) { + if (logChannel != null) { + logChannel.logError("Error refreshing Spark engine metrics", e); + } + } + } + }, + 0L, + 1000L); + } catch (Throwable e) { + setRunning(false); + setStopped(true); + setErrors(getErrors() + 1); + ExecutorUtil.cleanup(metricsRefreshTimer); + throw new HopException("Unexpected error starting Spark pipeline", e); + } finally { + Thread.currentThread().setContextClassLoader(previousCl); + } + } + + /** + * Seed the metrics grid with one placeholder component per transform so the UI lists them before + * executor slices arrive. + */ + private void seedEngineMetricsComponents() { + EngineMetrics em = new EngineMetrics(); + em.setStartDate(executionStartDate); + if (pipelineMeta != null) { + for (TransformMeta transformMeta : pipelineMeta.getTransforms()) { + EngineComponent component = new EngineComponent(transformMeta.getName(), 0); + component.setRunning(true); + component.setStatus(ComponentExecutionStatus.STATUS_RUNNING); + component.setExecutionStartDate(executionStartDate); + assignComponentIdentity(component); + em.addComponent(component); + em.setComponentRunning(component, true); + em.setComponentStatus(component, ComponentExecutionStatus.STATUS_RUNNING.getDescription()); + } + } + synchronized (this) { + engineMetrics = em; + } + } + + /** Stable log-channel id per transform copy for execution info + GUI. */ + private String componentLogChannelId(String transformName, int copyNr) { + String key = transformName + '\0' + copyNr; + return componentLogChannelIds.computeIfAbsent( + key, + k -> { + String parent = getLogChannelId(); + if (StringUtils.isEmpty(parent)) { + parent = "spark"; + } + return parent + "|" + transformName + "|" + copyNr; + }); + } + + private void assignComponentIdentity(EngineComponent component) { + if (component == null) { + return; + } + component.setLogChannelId(componentLogChannelId(component.getName(), component.getCopyNr())); + } + + /** + * Build {@link EngineMetrics} from Spark transform metric slices (mapPartitions) plus seeded + * placeholders for transforms that only have native Dataset handlers. + */ + protected synchronized void populateEngineMetrics() { + EngineMetrics em = new EngineMetrics(); + em.setStartDate(getExecutionStartDate()); + em.setEndDate(getExecutionEndDate()); + + Set transformsWithSlices = new HashSet<>(); + Map slices = + metricsAccumulator != null ? metricsAccumulator.value() : Collections.emptyMap(); + + for (SparkTransformMetricSlice slice : slices.values()) { + transformsWithSlices.add(slice.getTransformName()); + EngineComponent component = new EngineComponent(slice.getTransformName(), slice.getCopyNr()); + component.setLinesRead(slice.getLinesRead()); + component.setLinesWritten(slice.getLinesWritten()); + component.setLinesInput(slice.getLinesInput()); + component.setLinesOutput(slice.getLinesOutput()); + component.setErrors(slice.getErrors()); + applySliceTiming(component, slice); + if (StringUtils.isNotEmpty(slice.getHost())) { + component.setLogText("host=" + slice.getHost()); + } + + ComponentExecutionStatus componentStatus = resolveComponentStatus(slice); + component.setStatus(componentStatus); + component.setRunning(componentStatus == ComponentExecutionStatus.STATUS_RUNNING); + component.setStopped( + componentStatus == ComponentExecutionStatus.STATUS_STOPPED + || componentStatus == ComponentExecutionStatus.STATUS_HALTING); + assignComponentIdentity(component); + + em.addComponent(component); + em.setComponentMetric(component, Pipeline.METRIC_READ, slice.getLinesRead()); + em.setComponentMetric(component, Pipeline.METRIC_WRITTEN, slice.getLinesWritten()); + em.setComponentMetric(component, Pipeline.METRIC_INPUT, slice.getLinesInput()); + em.setComponentMetric(component, Pipeline.METRIC_OUTPUT, slice.getLinesOutput()); + em.setComponentMetric(component, Pipeline.METRIC_ERROR, slice.getErrors()); + em.setComponentStatus(component, componentStatus.getDescription()); + em.setComponentRunning(component, component.isRunning()); + long speedRows = + Math.max( + Math.max(slice.getLinesRead(), slice.getLinesWritten()), + Math.max(slice.getLinesInput(), slice.getLinesOutput())); + if (speedRows > 0) { + long durationMs = Math.max(1L, component.getExecutionDuration()); + double speed = (speedRows * 1000.0d) / durationMs; + em.setComponentSpeed(component, String.format("%.1f", speed)); + } + } + + // Transforms with no metric slices yet (not scheduled / no accumulator data) + if (pipelineMeta != null) { + ComponentExecutionStatus pipelineStatus = status; + for (TransformMeta transformMeta : pipelineMeta.getTransforms()) { + if (transformsWithSlices.contains(transformMeta.getName())) { + continue; + } + EngineComponent component = new EngineComponent(transformMeta.getName(), 0); + component.setExecutionStartDate(getExecutionStartDate()); + component.setExecutionEndDate(getExecutionEndDate()); + component.setExecutionDuration( + calculateDuration(getExecutionStartDate(), getExecutionEndDate())); + component.setStatus(pipelineStatus); + component.setRunning(pipelineStatus == ComponentExecutionStatus.STATUS_RUNNING); + component.setStopped( + pipelineStatus == ComponentExecutionStatus.STATUS_STOPPED || isStopped()); + assignComponentIdentity(component); + em.addComponent(component); + em.setComponentStatus(component, pipelineStatus.getDescription()); + em.setComponentRunning(component, component.isRunning()); + } + } + + engineMetrics = em; + } + + // --- Execution information location (Beam/Local pattern, driver-side) --- + + /** + * Load and initialize the execution information location named on the pipeline run configuration + * (if any). + */ + public void lookupExecutionInformationLocation() throws HopException { + if (pipelineRunConfiguration == null || metadataProvider == null) { + return; + } + String locationName = resolve(pipelineRunConfiguration.getExecutionInfoLocationName()); + if (StringUtils.isEmpty(locationName)) { + return; + } + ExecutionInfoLocation location = + metadataProvider.getSerializer(ExecutionInfoLocation.class).load(locationName); + if (location == null) { + logChannel.logError( + "Execution information location '" + + locationName + + "' could not be found in the metadata"); + return; + } + // Clone so nested engines do not share timer/rootFolder state on the same instance + executionInfoLocation = location.clone(); + executionInfoClosed = false; + IExecutionInfoLocation iLocation = executionInfoLocation.getExecutionInfoLocation(); + iLocation.initialize(this, metadataProvider); + logChannel.logBasic( + "Using execution information location '" + + locationName + + "' (logChannelId=" + + getLogChannelId() + + ")"); + } + + /** + * Register this pipeline execution at the configured location (metadata, variables, parameters). + */ + public void registerPipelineExecutionInformation() throws HopException { + if (executionInfoLocation == null) { + return; + } + executionInfoLocation + .getExecutionInfoLocation() + .registerExecution(ExecutionBuilder.fromExecutor(this).build()); + } + + /** Periodically push pipeline + transform state to the execution information location. */ + public void startExecutionInfoTimer() { + if (executionInfoLocation == null) { + return; + } + long delay = Const.toLong(resolve(executionInfoLocation.getDataLoggingDelay()), 2000L); + long interval = Const.toLong(resolve(executionInfoLocation.getDataLoggingInterval()), 5000L); + final IExecutionInfoLocation iLocation = executionInfoLocation.getExecutionInfoLocation(); + + executionInfoTimer = new Timer("hop-spark-execution-info", true); + executionInfoTimer.schedule( + new TimerTask() { + @Override + public void run() { + try { + if (isFinished() || isStopped() || executionInfoClosed) { + return; + } + // Refresh metrics so component state includes latest Spark counters + populateEngineMetrics(); + updatePipelineState(iLocation); + } catch (Exception e) { + if (logChannel != null) { + logChannel.logBasic( + "Warning: unable to register execution info at location " + + executionInfoLocation.getName() + + " (non-fatal): " + + e.getMessage()); + } + } + } + }, + delay, + interval); + } + + protected void updatePipelineState(IExecutionInfoLocation iLocation) throws HopException { + // Register sample rows collected on executors before updating parent/transform state + registerSampleDataFromExecutors(iLocation); + + ExecutionState executionState = + ExecutionStateBuilder.fromExecutor(SparkPipelineEngine.this, -1).build(); + iLocation.updateExecutionState(executionState); + + // Transform Execution + state nodes under the parent pipeline (Beam does the same from workers; + // we do it on the driver so caching locations keep hierarchy in one CacheEntry). + for (IEngineComponent component : getComponents()) { + registerTransformExecution(iLocation, component); + ExecutionState transformState = + ExecutionStateBuilder.fromTransform(SparkPipelineEngine.this, component).build(); + iLocation.updateExecutionState(transformState); + } + } + + /** + * Register a transform {@link Execution} child of this pipeline so the GUI can resolve parent → + * transform hierarchy (see {@code PipelineExecutionViewer.loadSelectedTransformData}). + */ + protected void registerTransformExecution( + IExecutionInfoLocation iLocation, IEngineComponent component) throws HopException { + if (iLocation == null || component == null) { + return; + } + String id = component.getLogChannelId(); + if (StringUtils.isEmpty(id)) { + id = componentLogChannelId(component.getName(), component.getCopyNr()); + } + Execution execution = + ExecutionBuilder.of() + .withId(id) + .withParentId(getLogChannelId()) + .withName(component.getName()) + .withCopyNr(Integer.toString(component.getCopyNr())) + .withExecutorType(ExecutionType.Transform) + .withExecutionStartDate( + component.getExecutionStartDate() != null + ? component.getExecutionStartDate() + : getExecutionStartDate()) + .build(); + iLocation.registerExecution(execution); + } + + /** + * Pull JSON {@link ExecutionData} samples from the Spark accumulator and register them on the + * driver-side execution info location (where the parent pipeline CacheEntry lives). + */ + protected void registerSampleDataFromExecutors(IExecutionInfoLocation iLocation) { + if (iLocation == null || sampleDataAccumulator == null || sampleDataAccumulator.isZero()) { + return; + } + try { + Map samples = sampleDataAccumulator.value(); + if (samples == null || samples.isEmpty()) { + return; + } + var mapper = HopJson.newMapper(); + int registered = 0; + for (Map.Entry entry : samples.entrySet()) { + try { + ExecutionData data = mapper.readValue(entry.getValue(), ExecutionData.class); + if (data != null) { + // Ensure parent id points at this pipeline execution + if (StringUtils.isEmpty(data.getParentId())) { + data.setParentId(getLogChannelId()); + } + // Ensure transform Execution child exists (ownerId is the child log channel id) + registerTransformExecutionFromSample(iLocation, data); + iLocation.registerData(data); + registered++; + } + } catch (Exception e) { + if (logChannel != null) { + logChannel.logError( + "Error registering executor sample data for owner '" + + entry.getKey() + + "' (non-fatal)", + e); + } + } + } + if (registered > 0 && logChannel != null) { + logChannel.logBasic( + "Registered " + registered + " transform sample set(s) from Spark executors"); + } + } catch (Exception e) { + if (logChannel != null) { + logChannel.logError("Error reading sample data accumulator (non-fatal)", e); + } + } + } + + /** + * Beam {@code TransformBaseFn.registerExecutingTransform}: child Execution id = sample owner id, + * parent = pipeline log channel id. + */ + private void registerTransformExecutionFromSample( + IExecutionInfoLocation iLocation, ExecutionData data) throws HopException { + if (StringUtils.isEmpty(data.getOwnerId())) { + return; + } + String transformName = null; + String copyNr = "0"; + if (data.getSetMetaData() != null) { + for (ExecutionDataSetMeta setMeta : data.getSetMetaData().values()) { + if (setMeta != null && StringUtils.isNotEmpty(setMeta.getName())) { + transformName = setMeta.getName(); + if (StringUtils.isNotEmpty(setMeta.getCopyNr())) { + copyNr = setMeta.getCopyNr(); + } + break; + } + } + } + if (StringUtils.isEmpty(transformName)) { + // ownerId format: parent|transformName|copyNr + String ownerId = data.getOwnerId(); + int first = ownerId.indexOf('|'); + int last = ownerId.lastIndexOf('|'); + if (first > 0 && last > first) { + transformName = ownerId.substring(first + 1, last); + copyNr = ownerId.substring(last + 1); + } else { + transformName = ownerId; + } + } + Execution execution = + ExecutionBuilder.of() + .withId(data.getOwnerId()) + .withParentId( + StringUtils.isNotEmpty(data.getParentId()) ? data.getParentId() : getLogChannelId()) + .withName(transformName) + .withCopyNr(copyNr) + .withExecutorType(ExecutionType.Transform) + .withExecutionStartDate(getExecutionStartDate()) + .build(); + iLocation.registerExecution(execution); + } + + /** Final pipeline/transform state update and close the location. Safe to call multiple times. */ + public void stopExecutionInfoTimer() { + if (executionInfoLocation == null || executionInfoClosed) { + ExecutorUtil.cleanup(executionInfoTimer); + return; + } + try { + ExecutorUtil.cleanup(executionInfoTimer); + executionInfoTimer = null; + + try { + populateEngineMetrics(); + } catch (Exception e) { + // ignore — still try to write state + } + + IExecutionInfoLocation iLocation = executionInfoLocation.getExecutionInfoLocation(); + // Final sample flush from executors (after jobs complete, accumulator is fully merged) + registerSampleDataFromExecutors(iLocation); + + ExecutionState executionState = + ExecutionStateBuilder.fromExecutor(SparkPipelineEngine.this, -1).build(); + iLocation.updateExecutionState(executionState); + + for (IEngineComponent component : getComponents()) { + registerTransformExecution(iLocation, component); + ExecutionState transformState = + ExecutionStateBuilder.fromTransform(SparkPipelineEngine.this, component).build(); + iLocation.updateExecutionState(transformState); + } + + iLocation.close(); + } catch (Throwable e) { + if (logChannel != null) { + logChannel.logError( + "Error writing final execution state to location (non-fatal): " + + (executionInfoLocation != null ? executionInfoLocation.getName() : "?"), + e); + } + } finally { + executionInfoClosed = true; + } + } + + /** + * Map partition wall-clock times onto {@link EngineComponent} fields the GUI Metrics grid uses + * for Duration ({@code firstRowReadDate} / {@code lastRowWrittenDate}). + */ + private void applySliceTiming(EngineComponent component, SparkTransformMetricSlice slice) { + long startMs = slice.getStartTimeMs(); + long endMs = slice.getEndTimeMs(); + if (startMs > 0) { + Date start = new Date(startMs); + component.setInitStartDate(start); + component.setExecutionStartDate(start); + component.setFirstRowReadDate(start); + } else if (getExecutionStartDate() != null) { + // Fallback for slices without timing + component.setExecutionStartDate(getExecutionStartDate()); + component.setFirstRowReadDate(getExecutionStartDate()); + } + + if (slice.isFinished() && endMs > 0) { + Date end = new Date(endMs); + component.setExecutionEndDate(end); + component.setLastRowWrittenDate(end); + } else if (slice.isFinished() && getExecutionEndDate() != null) { + component.setExecutionEndDate(getExecutionEndDate()); + component.setLastRowWrittenDate(getExecutionEndDate()); + } + // While running, leave lastRowWrittenDate null so the grid ticks duration live + + long durationMs = slice.durationMs(); + if (durationMs <= 0 && startMs > 0) { + durationMs = + calculateDuration(component.getFirstRowReadDate(), component.getLastRowWrittenDate()); + } + if (durationMs <= 0) { + durationMs = calculateDuration(getExecutionStartDate(), getExecutionEndDate()); + } + component.setExecutionDuration(durationMs); + } + + private ComponentExecutionStatus resolveComponentStatus(SparkTransformMetricSlice slice) { + if (isStopped() || getErrors() > 0) { + if (slice.isFinished() || slice.getErrors() > 0) { + return ComponentExecutionStatus.STATUS_STOPPED; + } + } + if (slice.getErrors() > 0) { + return ComponentExecutionStatus.STATUS_STOPPED; + } + if (slice.isFinished() || isFinished()) { + return ComponentExecutionStatus.STATUS_FINISHED; + } + if (slice.isRunning() || isRunning()) { + return ComponentExecutionStatus.STATUS_RUNNING; + } + return status != null ? status : ComponentExecutionStatus.STATUS_EMPTY; + } + + protected long calculateDuration(Date startTime, Date stopTime) { + if (startTime != null && stopTime == null) { + return Calendar.getInstance().getTimeInMillis() - startTime.getTime(); + } + if (startTime != null) { + return stopTime.getTime() - startTime.getTime(); + } + return 0L; + } + + protected SparkSession createSparkSession() throws HopException { + ClassLoader pluginCl = getClass().getClassLoader(); + ClassLoader previousCl = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(pluginCl); + SparkSession session = buildSparkSession(); + // Ship project package zip to every executor (SparkFiles) when HOP_SPARK_PROJECT_PACKAGE set + distributeProjectPackageIfConfigured(session); + return session; + } catch (IllegalStateException e) { + // Surface a actionable message when classic SparkSession cannot be loaded + throw new IllegalStateException( + e.getMessage() + + " Ensure the native Spark engine plugin ships spark-sql, hadoop-client-api/runtime," + + " and log4j under plugins/engines/spark/lib (Spark 4 loads" + + " org.apache.spark.sql.classic.SparkSession reflectively).", + e); + } finally { + Thread.currentThread().setContextClassLoader(previousCl); + } + } + + /** + * Native Spark uses RDDs, accumulators, and {@code SparkContext} APIs. Databricks Spark Connect + * sessions (cluster access mode Standard / shared, or Serverless) do not expose + * a real SparkContext. Fail early with an actionable message. + */ + static void requireClassicSparkContext(SparkSession session) throws HopException { + if (session == null) { + throw new HopException("SparkSession is null"); + } + String sessionClass = session.getClass().getName(); + try { + // Touches the real context; Connect client throws SparkUnsupportedOperationException. + session.sparkContext().appName(); + } catch (Throwable t) { + String msg = t.getMessage() != null ? t.getMessage() : t.getClass().getSimpleName(); + if (isSparkConnectContextError(sessionClass, msg, t)) { + throw new HopException( + "Apache Hop Native Spark requires a classic SparkContext (RDD / accumulator APIs). " + + "This session is Spark Connect (" + + sessionClass + + ") which does not expose SparkContext on Databricks " + + "Standard access mode or Serverless compute. " + + "Use a classic job cluster (Deploy & run with cluster field = new_cluster) or an " + + "all-purpose cluster with access mode Dedicated (single user), not Standard/shared " + + "or Serverless. Underlying error: " + + msg, + t); + } + throw new HopException("Unable to access SparkContext from SparkSession: " + msg, t); + } + } + + static boolean isSparkConnectContextError(String sessionClass, String message, Throwable t) { + if (sessionClass != null && sessionClass.contains("connect")) { + return true; + } + if (message != null) { + String m = message.toLowerCase(); + if (m.contains("spark connect") + || m.contains("unsupported_connect") + || m.contains("session_spark_context") + || m.contains("standard access mode") + || m.contains("serverless")) { + return true; + } + } + String tn = t != null ? t.getClass().getName() : ""; + return tn.contains("SparkUnsupportedOperationException") || tn.contains("connect"); + } + + /** + * When a Native Spark project package is configured, make it available on executors (shared path + * or {@code SparkContext.addFile}) and re-materialize {@code PROJECT_HOME} on the driver. + */ + private void distributeProjectPackageIfConfigured(SparkSession session) throws HopException { + String packageUri = getVariable(SparkProjectPackage.VAR_PACKAGE_URI); + if (StringUtils.isEmpty(packageUri)) { + return; + } + SparkProjectPackage.distributeToCluster(session, this); + SparkProjectPackage.ensureMaterializedOnWorker(this); + if (logChannel != null) { + String sparkFile = getVariable(SparkProjectPackage.VAR_PACKAGE_SPARK_FILE); + String source = getVariable(SparkProjectPackage.VAR_PACKAGE_URI); + if (StringUtils.isNotEmpty(sparkFile)) { + logChannel.logBasic( + "Distributed Spark project package for executors (SparkFiles=" + + sparkFile + + ", source=" + + source + + "); PROJECT_HOME=" + + getVariable("PROJECT_HOME")); + } else { + logChannel.logBasic( + "Using Spark project package URI only (no SparkFiles basename): " + + source + + "; PROJECT_HOME=" + + getVariable("PROJECT_HOME") + + " — workers must open this path directly"); + } + } + } + + private SparkSession buildSparkSession() throws HopException { + LakeSessionPlan lakePlan = LakeSessionPlan.from(pipelineMeta, metadataProvider, this); + if (!lakePlan.isEmpty()) { + lakePlan.verifyClasspath(getClass().getClassLoader()); + logChannel.logBasic( + "Lakehouse session plan formats=" + + lakePlan.getFormatsNeeded() + + " catalogs=" + + lakePlan.getCatalogsByMetaName().keySet()); + } + + // Nested under another Native Spark pipeline: always reuse the parent's session when present. + if (parentPipeline instanceof SparkPipelineEngine parentSpark + && parentSpark.sparkSession != null) { + sessionOwnedByThisEngine = false; + SparkSession session = parentSpark.sparkSession; + logChannel.logBasic( + "Nested Native Spark pipeline reusing parent SparkSession (parent='" + + (parentSpark.pipelineMeta != null ? parentSpark.pipelineMeta.getName() : "?") + + "', version=" + + session.version() + + ")"); + if (!lakePlan.isEmpty()) { + lakePlan.verifyActiveSession(session, logChannel, this); + } + return session; + } + + // spark-submit / existing driver: reuse the active session if present + Option active = SparkSession.getActiveSession(); + if (active.isDefined()) { + sessionOwnedByThisEngine = false; + SparkSession session = active.get(); + logChannel.logBasic( + "Reusing active SparkSession (version=" + + session.version() + + ", spark-submit or existing driver context)"); + if (!lakePlan.isEmpty()) { + lakePlan.verifyActiveSession(session, logChannel, this); + } + return session; + } + Option def = SparkSession.getDefaultSession(); + if (def.isDefined()) { + sessionOwnedByThisEngine = false; + SparkSession session = def.get(); + logChannel.logBasic("Reusing default SparkSession (version=" + session.version() + ")"); + if (!lakePlan.isEmpty()) { + lakePlan.verifyActiveSession(session, logChannel, this); + } + return session; + } + + boolean underSubmit = isRunningUnderSparkSubmit(); + String master = resolve(sparkEngineRunConfiguration.getSparkMaster()); + if (StringUtils.isEmpty(master)) { + // Prefer master from spark-submit / SparkConf when run config leaves it blank + String confMaster = System.getProperty("spark.master"); + if (StringUtils.isEmpty(confMaster)) { + try { + SparkConf conf = new SparkConf(true); + if (conf.contains("spark.master")) { + confMaster = conf.get("spark.master"); + } + } catch (Exception e) { + // ignore — fall back to local + } + } + master = StringUtils.isNotEmpty(confMaster) ? confMaster : "local[*]"; + } + String appName = resolve(sparkEngineRunConfiguration.getSparkAppName()); + if (StringUtils.isEmpty(appName)) { + appName = "Apache Hop - " + pipelineMeta.getName(); + } + + SparkSession.Builder builder = SparkSession.builder().appName(appName); + + // Under spark-submit, master is already in the conf; re-setting can still work but prefer + // not fighting the launcher when blank was intended to inherit. + if (!underSubmit + || StringUtils.isNotEmpty(resolve(sparkEngineRunConfiguration.getSparkMaster()))) { + builder = builder.master(master); + } else if (StringUtils.isNotEmpty(master)) { + builder = builder.master(master); + } + + applySparkConfig(builder, "spark.driver.memory", sparkEngineRunConfiguration.getDriverMemory()); + applySparkConfig( + builder, "spark.executor.memory", sparkEngineRunConfiguration.getExecutorMemory()); + applySparkConfig( + builder, "spark.executor.cores", sparkEngineRunConfiguration.getExecutorCores()); + + // Do not set spark.jars under spark-submit — the application jar is already the fat jar. + // Setting it again can confuse class loading. + if (!underSubmit) { + String fatJar = resolve(sparkEngineRunConfiguration.getFatJar()); + if (StringUtils.isNotEmpty(fatJar)) { + builder.config("spark.jars", fatJar); + } + } + + String extra = sparkEngineRunConfiguration.getSparkConfigs(); + if (StringUtils.isNotEmpty(extra)) { + for (String line : extra.split("\\r?\\n")) { + String trimmed = line.trim(); + if (trimmed.isEmpty() || trimmed.startsWith("#")) { + continue; + } + int eq = trimmed.indexOf('='); + if (eq > 0) { + builder.config( + resolve(trimmed.substring(0, eq).trim()), resolve(trimmed.substring(eq + 1).trim())); + } + } + } + + // Lakehouse: Delta/Iceberg extensions, hop_iceberg PATH catalog, SparkCatalog metadata. + // Applied after run-config sparkConfigs so lake defaults fill gaps; explicit run-config + // keys already set above win if the user overrode them (except catalog apply overwrites). + if (!lakePlan.isEmpty()) { + lakePlan.applyToBuilder(builder, this); + } + + // Defaults for local/dev only when not already set by submit or run config extras + if (!underSubmit && System.getProperty("spark.ui.enabled") == null) { + builder.config("spark.ui.enabled", "false"); + } + if (!underSubmit && System.getProperty("spark.sql.shuffle.partitions") == null) { + builder.config("spark.sql.shuffle.partitions", "4"); + } + + SparkSession session = builder.getOrCreate(); + // getOrCreate may still return a shared session under spark-submit; only claim ownership + // when we are not nested and no prior active session existed (we already returned above). + sessionOwnedByThisEngine = true; + logChannel.logBasic( + "Created SparkSession version=" + session.version() + " master=" + master + " (owned)"); + return session; + } + + /** + * Heuristic: spark-submit sets deploy mode / app id properties and loads SPARK_ENV / conf before + * main(). + */ + private static boolean isRunningUnderSparkSubmit() { + if (System.getenv("SPARK_ENV_LOADED") != null) { + return true; + } + if (System.getProperty("spark.submit.deployMode") != null) { + return true; + } + if (System.getProperty("spark.app.id") != null) { + return true; + } + // spark-submit always sets spark.master before invoking main + String master = System.getProperty("spark.master"); + return StringUtils.isNotEmpty(master) && !master.startsWith("local"); + } + + private void applySparkConfig(SparkSession.Builder builder, String key, String value) { + String resolved = resolve(value); + if (StringUtils.isNotEmpty(resolved)) { + builder.config(key, resolved); + } + } + + @Override + public void execute() throws HopException { + prepareExecution(); + startThreads(); + } + + @Override + public void waitUntilFinished() { + while ((running || paused || readyToStart) && !(stopped || finished)) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + if (sparkThread != null) { + try { + sparkThread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + @Override + public void stopAll() { + setStopped(true); + setRunning(false); + ExecutorUtil.cleanup(metricsRefreshTimer); + // Nested / reused sessions share the parent's SparkContext — never cancelAllJobs there. + if (sparkSession != null && sessionOwnedByThisEngine) { + try { + sparkSession.sparkContext().cancelAllJobs(); + } catch (Exception e) { + if (logChannel != null) { + logChannel.logError("Error cancelling Spark jobs", e); + } + } + } else if (sparkSession != null && logChannel != null) { + logChannel.logBasic( + "Skipping Spark job cancel for nested/reused session (pipeline='" + + (pipelineMeta != null ? pipelineMeta.getName() : "?") + + "')"); + } + try { + populateEngineMetrics(); + } catch (Exception e) { + if (logChannel != null) { + logChannel.logError("Error populating metrics after stop", e); + } + } + stopExecutionInfoTimer(); + try { + fireExecutionStoppedListeners(); + } catch (HopException e) { + if (logChannel != null) { + logChannel.logError("Error firing stopped listeners", e); + } + } + } + + @Override + public void cleanup() { + ExecutorUtil.cleanup(metricsRefreshTimer); + ExecutorUtil.cleanup(executionInfoTimer); + if (sparkSession != null) { + try { + // Only stop sessions this engine created. Nested Pipeline Executor children and + // spark-submit drivers must leave the shared SparkSession alone. + if (sessionOwnedByThisEngine) { + String master = + sparkEngineRunConfiguration != null + ? resolve(sparkEngineRunConfiguration.getSparkMaster()) + : ""; + if (master != null && master.startsWith("local")) { + logChannel.logBasic( + "Stopping owned local SparkSession for pipeline '" + + (pipelineMeta != null ? pipelineMeta.getName() : "?") + + "'"); + sparkSession.stop(); + } + } else if (logChannel != null) { + logChannel.logDetailed( + "Leaving shared SparkSession running after nested/reused pipeline '" + + (pipelineMeta != null ? pipelineMeta.getName() : "?") + + "'"); + } + } catch (Exception e) { + if (logChannel != null) { + logChannel.logError("Error stopping SparkSession", e); + } + } finally { + sparkSession = null; + } + } + } + + /** Whether this engine owns (created) the SparkSession and may stop it. */ + boolean isSessionOwnedByThisEngine() { + return sessionOwnedByThisEngine; + } + + @Override + public EngineCompatibility supports(IPlugin transformPlugin) { + if (transformPlugin == null) { + return EngineCompatibility.unknown(); + } + String[] ids = transformPlugin.getIds(); + if (ids != null) { + for (String id : ids) { + String ban = HopPipelineMetaToSparkConverter.HARD_BANNED_PLUGIN_IDS.get(id); + if (ban != null) { + return EngineCompatibility.unsupported(ban); + } + if (HopPipelineMetaToSparkConverter.EXPLICIT_HANDLER_PLUGIN_IDS.contains(id)) { + return EngineCompatibility.supported(); + } + } + } + // No opinion for ordinary transforms — generic mapPartitions may still work + return EngineCompatibility.unknown(); + } + + @Override + public EngineMetrics getEngineMetrics() { + return engineMetrics; + } + + @Override + public EngineMetrics getEngineMetrics(String componentName, int copyNr) { + EngineMetrics em = new EngineMetrics(); + em.setStartDate(engineMetrics.getStartDate()); + em.setEndDate(engineMetrics.getEndDate()); + for (IEngineComponent component : engineMetrics.getComponents()) { + if (componentName != null && !component.getName().equalsIgnoreCase(componentName)) { + continue; + } + if (copyNr >= 0 && component.getCopyNr() != copyNr) { + continue; + } + em.addComponent(component); + for (IEngineMetric metric : engineMetrics.getMetricsList()) { + Long value = engineMetrics.getComponentMetric(component, metric); + if (value != null) { + em.setComponentMetric(component, metric, value); + } + } + Boolean running = engineMetrics.getComponentRunningMap().get(component); + if (running != null) { + em.setComponentRunning(component, running); + } + String componentStatus = engineMetrics.getComponentStatusMap().get(component); + if (componentStatus != null) { + em.setComponentStatus(component, componentStatus); + } + String speed = engineMetrics.getComponentSpeedMap().get(component); + if (speed != null) { + em.setComponentSpeed(component, speed); + } + } + return em; + } + + @Override + public Result getResult() { + Result result = new Result(); + result.setNrErrors(errors); + result.setResult(errors == 0); + result.setStopped(isStopped()); + result.setLogChannelId(getLogChannelId()); + // Roll up lines from engine metrics when available + long linesRead = 0; + long linesWritten = 0; + long linesInput = 0; + long linesOutput = 0; + for (IEngineComponent component : engineMetrics.getComponents()) { + linesRead += nullToZero(engineMetrics.getComponentMetric(component, Pipeline.METRIC_READ)); + linesWritten += + nullToZero(engineMetrics.getComponentMetric(component, Pipeline.METRIC_WRITTEN)); + linesInput += nullToZero(engineMetrics.getComponentMetric(component, Pipeline.METRIC_INPUT)); + linesOutput += + nullToZero(engineMetrics.getComponentMetric(component, Pipeline.METRIC_OUTPUT)); + } + result.setNrLinesRead(linesRead); + result.setNrLinesWritten(linesWritten); + result.setNrLinesInput(linesInput); + result.setNrLinesOutput(linesOutput); + return result; + } + + private static long nullToZero(Long value) { + return value == null ? 0L : value; + } + + @Override + public void pauseExecution() { + // Not supported + } + + @Override + public void resumeExecution() { + // Not supported + } + + @Override + public boolean hasHaltedComponents() { + return hasHaltedComponents; + } + + @Override + public String getComponentLogText(String componentName, int copyNr) { + return ""; + } + + @Override + public List getComponents() { + return engineMetrics.getComponents(); + } + + @Override + public List getComponentCopies(String name) { + List copies = new ArrayList<>(); + for (IEngineComponent component : engineMetrics.getComponents()) { + if (component.getName().equalsIgnoreCase(name)) { + copies.add(component); + } + } + return copies; + } + + @Override + public IEngineComponent findComponent(String name, int copyNr) { + for (IEngineComponent component : engineMetrics.getComponents()) { + if (component.getName().equalsIgnoreCase(name) && component.getCopyNr() == copyNr) { + return component; + } + } + return null; + } + + @Override + public void retrieveComponentOutput( + IVariables variables, + String componentName, + int copyNr, + int nrRows, + IPipelineComponentRowsReceived rowsReceived) + throws HopException { + throw new HopException( + "Retrieving component output is not supported by the native Spark pipeline engine"); + } + + @Override + public boolean isSafeModeEnabled() { + return false; + } + + @Override + public IRowSet findRowSet( + String fromTransformName, + int fromTransformCopy, + String toTransformName, + int toTransformCopy) { + return null; + } + + @Override + public void pipelineCompleted() throws HopException { + stopExecutionInfoTimer(); + cleanup(); + } + + // --- listeners --- + + @Override + public void addExecutionStartedListener( + IExecutionStartedListener> listener) { + executionStartedListeners.add(listener); + } + + @Override + public void removeExecutionStartedListener( + IExecutionStartedListener> listener) { + executionStartedListeners.remove(listener); + } + + @Override + public void firePipelineExecutionStartedListeners() throws HopException { + fireExecutionStartedListeners(); + } + + @Override + public void fireExecutionStartedListeners() throws HopException { + synchronized (executionStartedListeners) { + for (IExecutionStartedListener> listener : + executionStartedListeners) { + listener.started(this); + } + } + } + + @Override + public void addExecutionFinishedListener( + IExecutionFinishedListener> listener) { + executionFinishedListeners.add(listener); + } + + @Override + public void removeExecutionFinishedListener( + IExecutionFinishedListener> listener) { + executionFinishedListeners.remove(listener); + } + + @Override + public void firePipelineExecutionFinishedListeners() throws HopException { + fireExecutionFinishedListeners(); + } + + @Override + public void fireExecutionFinishedListeners() throws HopException { + synchronized (executionFinishedListeners) { + for (IExecutionFinishedListener> listener : + executionFinishedListeners) { + listener.finished(this); + } + } + } + + @Override + public void addExecutionStoppedListener( + IExecutionStoppedListener> listener) { + executionStoppedListeners.add(listener); + } + + @Override + public void removeExecutionStoppedListener( + IExecutionStoppedListener> listener) { + executionStoppedListeners.remove(listener); + } + + @Override + public void firePipelineExecutionStoppedListeners() throws HopException { + fireExecutionStoppedListeners(); + } + + @Override + public void fireExecutionStoppedListeners() throws HopException { + synchronized (executionStoppedListeners) { + for (IExecutionStoppedListener> listener : + executionStoppedListeners) { + listener.stopped(this); + } + } + } + + @Override + public > + void addExecutionDataSampler(Sampler sampler) { + dataSamplers.add(sampler); + } + + // --- logging object --- + + @Override + public String getObjectName() { + return pipelineMeta != null ? pipelineMeta.getName() : SparkConst.PLUGIN_NAME; + } + + @Override + public String getFilename() { + return pipelineMeta != null ? pipelineMeta.getFilename() : null; + } + + @Override + public LoggingObjectType getObjectType() { + return LoggingObjectType.PIPELINE; + } + + @Override + public String getObjectCopy() { + return null; + } + + @Override + public Date getRegistrationDate() { + return null; + } + + @Override + public boolean isGatheringMetrics() { + return logChannel != null && logChannel.isGatheringMetrics(); + } + + @Override + public void setGatheringMetrics(boolean gatheringMetrics) { + if (logChannel != null) { + logChannel.setGatheringMetrics(gatheringMetrics); + } + } + + @Override + public void setForcingSeparateLogging(boolean forcingSeparateLogging) { + if (logChannel != null) { + logChannel.setForcingSeparateLogging(forcingSeparateLogging); + } + } + + @Override + public boolean isForcingSeparateLogging() { + return logChannel != null && logChannel.isForcingSeparateLogging(); + } + + @Override + public String getLogChannelId() { + return logChannel != null ? logChannel.getLogChannelId() : null; + } + + @Override + public ILogChannel getLogChannel() { + return logChannel; + } + + @Override + public void setLogChannel(ILogChannel log) { + this.logChannel = log; + } + + @Override + public void setParent(ILoggingObject parent) { + this.parent = parent; + } + + @Override + public ILoggingObject getParent() { + return parent; + } + + // --- parameters --- + + @Override + public void addParameterDefinition(String key, String defValue, String description) + throws DuplicateParamException { + namedParams.addParameterDefinition(key, defValue, description); + } + + @Override + public String getParameterDescription(String key) throws UnknownParamException { + return namedParams.getParameterDescription(key); + } + + @Override + public String getParameterDefault(String key) throws UnknownParamException { + return namedParams.getParameterDefault(key); + } + + @Override + public String getParameterValue(String key) throws UnknownParamException { + return namedParams.getParameterValue(key); + } + + @Override + public String[] listParameters() { + return namedParams.listParameters(); + } + + @Override + public void setParameterValue(String key, String value) throws UnknownParamException { + namedParams.setParameterValue(key, value); + } + + @Override + public void removeAllParameters() { + namedParams.removeAllParameters(); + } + + @Override + public void clearParameterValues() { + namedParams.clearParameterValues(); + } + + @Override + public void copyParametersFromDefinitions(INamedParameterDefinitions definitions) { + namedParams.copyParametersFromDefinitions(definitions); + } + + @Override + public void activateParameters(IVariables variables) { + namedParams.activateParameters(variables); + } + + // --- getters / setters --- + + @Override + public PipelineMeta getPipelineMeta() { + return pipelineMeta; + } + + @Override + public void setPipelineMeta(PipelineMeta pipelineMeta) { + this.pipelineMeta = pipelineMeta; + } + + @Override + public String getPluginId() { + return pluginId; + } + + @Override + public void setPluginId(String pluginId) { + this.pluginId = pluginId; + } + + @Override + public void setPipelineRunConfiguration(PipelineRunConfiguration pipelineRunConfiguration) { + this.pipelineRunConfiguration = pipelineRunConfiguration; + } + + @Override + public PipelineRunConfiguration getPipelineRunConfiguration() { + return pipelineRunConfiguration; + } + + @Override + public PipelineEngineCapabilities getEngineCapabilities() { + return engineCapabilities; + } + + @Override + public boolean isPreparing() { + return preparing; + } + + @Override + public boolean isReadyToStart() { + return readyToStart; + } + + @Override + public boolean isRunning() { + return running; + } + + @Override + public boolean isFinished() { + return finished; + } + + @Override + public boolean isStopped() { + return stopped; + } + + @Override + public boolean isPaused() { + return paused; + } + + @Override + public int getErrors() { + return errors; + } + + @Override + public void setMetadataProvider(IHopMetadataProvider metadataProvider) { + this.metadataProvider = metadataProvider; + } + + @Override + public IHopMetadataProvider getMetadataProvider() { + return metadataProvider; + } + + @Override + public void setLogLevel(LogLevel logLevel) { + this.logLevel = logLevel; + if (logChannel != null) { + logChannel.setLogLevel(logLevel); + } + } + + @Override + public LogLevel getLogLevel() { + return logLevel; + } + + @Override + public void setPreview(boolean preview) { + this.preview = preview; + } + + @Override + public boolean isPreview() { + return preview; + } + + @Override + public void setPreviousResult(Result previousResult) { + this.previousResult = previousResult; + } + + @Override + public Result getPreviousResult() { + return previousResult; + } + + @Override + public Date getExecutionStartDate() { + return executionStartDate; + } + + @Override + public Date getExecutionEndDate() { + return executionEndDate; + } + + @Override + public void addActiveSubPipeline(String transformName, IPipelineEngine executorPipeline) { + activeSubPipelines.put(transformName, executorPipeline); + } + + @Override + public IPipelineEngine getActiveSubPipeline(String subPipelineName) { + return activeSubPipelines.get(subPipelineName); + } + + @Override + public void addActiveSubWorkflow( + String subWorkflowName, IWorkflowEngine subWorkflow) { + activeSubWorkflows.put(subWorkflowName, subWorkflow); + } + + @Override + public IWorkflowEngine getActiveSubWorkflow(String subWorkflowName) { + return activeSubWorkflows.get(subWorkflowName); + } + + @Override + public void setInternalHopVariables(IVariables var) { + var.setVariable( + Const.INTERNAL_VARIABLE_PIPELINE_NAME, + Const.NVL(pipelineMeta != null ? pipelineMeta.getName() : "", "")); + var.setVariable( + Const.INTERNAL_VARIABLE_PIPELINE_ID, + logChannel != null ? logChannel.getLogChannelId() : ""); + } + + @Override + public void setContainerId(String containerId) { + this.containerId = containerId; + } + + @Override + public String getContainerId() { + return containerId; + } + + @Override + public String getStatusDescription() { + return statusDescription; + } + + @Override + public Map getExtensionDataMap() { + return extensionDataMap; + } + + @Override + public IPipelineEngine getParentPipeline() { + return parentPipeline; + } + + @Override + public void setParentPipeline(IPipelineEngine parentPipeline) { + this.parentPipeline = parentPipeline; + } + + @Override + public IWorkflowEngine getParentWorkflow() { + return parentWorkflow; + } + + @Override + public void setParentWorkflow(IWorkflowEngine parentWorkflow) { + this.parentWorkflow = parentWorkflow; + } + + @Override + public boolean isFeedbackShown() { + return false; + } + + @Override + public int getFeedbackSize() { + return 0; + } + + public ComponentExecutionStatus getStatus() { + return status; + } + + /** Exposed for tests. */ + public Dataset getResultDataset() { + return resultDataset; + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/engines/SparkPipelineEngineCapabilities.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/engines/SparkPipelineEngineCapabilities.java new file mode 100644 index 00000000000..d800f98f4ae --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/engines/SparkPipelineEngineCapabilities.java @@ -0,0 +1,28 @@ +/* + * 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.hop.spark.engines; + +import org.apache.hop.pipeline.engine.PipelineEngineCapabilities; + +/** Native Spark engine: no preview/debug/sniff/pause in v1. */ +public class SparkPipelineEngineCapabilities extends PipelineEngineCapabilities { + + public SparkPipelineEngineCapabilities() { + super(false, false, false, false); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/engines/SparkPipelineRunConfiguration.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/engines/SparkPipelineRunConfiguration.java new file mode 100644 index 00000000000..e3d478990cf --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/engines/SparkPipelineRunConfiguration.java @@ -0,0 +1,236 @@ +/* + * 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.hop.spark.engines; + +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import org.apache.hop.core.gui.plugin.GuiElementType; +import org.apache.hop.core.gui.plugin.GuiPlugin; +import org.apache.hop.core.gui.plugin.GuiWidgetElement; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.i18n.BaseMessages; +import org.apache.hop.metadata.api.HopMetadataProperty; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.config.PipelineRunConfiguration; +import org.apache.hop.pipeline.engines.EmptyPipelineRunConfiguration; +import org.apache.hop.spark.engines.template.SparkRunConfigTemplate; +import org.apache.hop.spark.util.SparkConst; +import org.apache.hop.spark.util.SparkRunMode; +import org.apache.hop.ui.core.dialog.EnterSelectionDialog; +import org.apache.hop.ui.hopgui.HopGui; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.MessageBox; +import org.eclipse.swt.widgets.Shell; + +@GuiPlugin +@Getter +@Setter +public class SparkPipelineRunConfiguration extends EmptyPipelineRunConfiguration + implements ISparkPipelineEngineRunConfiguration, Cloneable { + + private static final Class PKG = SparkPipelineRunConfiguration.class; + + /** + * Fill the form from a named deployment template (local, standalone, submit, Databricks, …). + * Mutates {@code object} (the live editor model); widgets are refreshed by GuiCompositeWidgets. + */ + @GuiWidgetElement( + id = "load-spark-config-template", + order = "19990-spark-options", + parentId = PipelineRunConfiguration.GUI_PLUGIN_ELEMENT_PARENT_ID, + type = GuiElementType.BUTTON, + label = "i18n::SparkEngine.LoadTemplate.Label", + toolTip = "i18n::SparkEngine.LoadTemplate.ToolTip") + public void loadConfigurationTemplate(Object object) { + if (!(object instanceof SparkPipelineRunConfiguration config)) { + return; + } + Shell shell = HopGui.getInstance().getShell(); + String[] labels = SparkRunConfigTemplate.displayNames(); + EnterSelectionDialog dialog = + new EnterSelectionDialog( + shell, + labels, + BaseMessages.getString(PKG, "SparkEngine.LoadTemplate.Dialog.Title"), + BaseMessages.getString(PKG, "SparkEngine.LoadTemplate.Dialog.Message")); + dialog.setAvoidQuickSearch(); + String choice = dialog.open(); + if (choice == null) { + return; + } + SparkRunConfigTemplate template = SparkRunConfigTemplate.fromDisplayName(choice); + if (template == null) { + return; + } + if (SparkRunConfigTemplate.looksCustomized(config)) { + MessageBox box = new MessageBox(shell, SWT.YES | SWT.NO | SWT.ICON_QUESTION); + box.setText(BaseMessages.getString(PKG, "SparkEngine.LoadTemplate.Confirm.Title")); + box.setMessage( + BaseMessages.getString( + PKG, "SparkEngine.LoadTemplate.Confirm.Message", template.getDisplayName())); + if (box.open() != SWT.YES) { + return; + } + } + template.applyTo(config); + } + + @GuiWidgetElement( + order = "20000-spark-options", + parentId = PipelineRunConfiguration.GUI_PLUGIN_ELEMENT_PARENT_ID, + type = GuiElementType.TEXT, + label = "i18n::SparkEngine.OptionsMaster.Label", + toolTip = "i18n::SparkEngine.OptionsMaster.ToolTip") + @HopMetadataProperty + private String sparkMaster; + + @GuiWidgetElement( + order = "20010-spark-options", + parentId = PipelineRunConfiguration.GUI_PLUGIN_ELEMENT_PARENT_ID, + type = GuiElementType.TEXT, + label = "i18n::SparkEngine.OptionsAppName.Label", + toolTip = "i18n::SparkEngine.OptionsAppName.ToolTip") + @HopMetadataProperty + private String sparkAppName; + + @GuiWidgetElement( + order = "20020-spark-options", + parentId = PipelineRunConfiguration.GUI_PLUGIN_ELEMENT_PARENT_ID, + type = GuiElementType.FILENAME, + label = "i18n::SparkEngine.OptionsFatJar.Label", + toolTip = "i18n::SparkEngine.OptionsFatJar.ToolTip") + @HopMetadataProperty + private String fatJar; + + @GuiWidgetElement( + order = "20030-spark-options", + parentId = PipelineRunConfiguration.GUI_PLUGIN_ELEMENT_PARENT_ID, + type = GuiElementType.MULTI_LINE_TEXT, + multiLineTextHeight = 5, + label = "i18n::SparkEngine.OptionsConfigs.Label", + toolTip = "i18n::SparkEngine.OptionsConfigs.ToolTip") + @HopMetadataProperty + private String sparkConfigs; + + @GuiWidgetElement( + order = "20040-spark-options", + parentId = PipelineRunConfiguration.GUI_PLUGIN_ELEMENT_PARENT_ID, + type = GuiElementType.TEXT, + label = "i18n::SparkEngine.OptionsDriverMemory.Label", + toolTip = "i18n::SparkEngine.OptionsDriverMemory.ToolTip") + @HopMetadataProperty + private String driverMemory; + + @GuiWidgetElement( + order = "20050-spark-options", + parentId = PipelineRunConfiguration.GUI_PLUGIN_ELEMENT_PARENT_ID, + type = GuiElementType.TEXT, + label = "i18n::SparkEngine.OptionsExecutorMemory.Label", + toolTip = "i18n::SparkEngine.OptionsExecutorMemory.ToolTip") + @HopMetadataProperty + private String executorMemory; + + @GuiWidgetElement( + order = "20060-spark-options", + parentId = PipelineRunConfiguration.GUI_PLUGIN_ELEMENT_PARENT_ID, + type = GuiElementType.TEXT, + label = "i18n::SparkEngine.OptionsExecutorCores.Label", + toolTip = "i18n::SparkEngine.OptionsExecutorCores.ToolTip") + @HopMetadataProperty + private String executorCores; + + @GuiWidgetElement( + order = "20070-spark-options", + parentId = PipelineRunConfiguration.GUI_PLUGIN_ELEMENT_PARENT_ID, + type = GuiElementType.FOLDER, + label = "i18n::SparkEngine.OptionsTempLocation.Label", + toolTip = "i18n::SparkEngine.OptionsTempLocation.ToolTip") + @HopMetadataProperty + private String tempLocation; + + @GuiWidgetElement( + order = "20080-spark-options", + parentId = PipelineRunConfiguration.GUI_PLUGIN_ELEMENT_PARENT_ID, + type = GuiElementType.TEXT, + label = "i18n::SparkEngine.OptionsPluginsToStage.Label", + toolTip = "i18n::SparkEngine.OptionsPluginsToStage.ToolTip") + @HopMetadataProperty + private String pluginsToStage; + + @GuiWidgetElement( + order = "20090-spark-options", + parentId = PipelineRunConfiguration.GUI_PLUGIN_ELEMENT_PARENT_ID, + type = GuiElementType.MULTI_LINE_TEXT, + multiLineTextHeight = 4, + label = "i18n::SparkEngine.OptionsPathSchemeMap.Label", + toolTip = "i18n::SparkEngine.OptionsPathSchemeMap.ToolTip") + @HopMetadataProperty + private String pathSchemeMap; + + /** + * Default for generic mapPartitions transforms ({@link SparkRunMode#DISTRIBUTED} or {@link + * SparkRunMode#DRIVER_ONLY}). Overridable per transform via the "Spark Run Mode" context action. + */ + @GuiWidgetElement( + id = "genericTransformRunMode", + order = "20100-spark-options", + parentId = PipelineRunConfiguration.GUI_PLUGIN_ELEMENT_PARENT_ID, + type = GuiElementType.COMBO, + label = "i18n::SparkEngine.OptionsGenericRunMode.Label", + toolTip = "i18n::SparkEngine.OptionsGenericRunMode.ToolTip", + comboValuesMethod = "getGenericTransformRunModes") + @HopMetadataProperty(key = "generic_transform_run_mode") + private String genericTransformRunMode; + + public SparkPipelineRunConfiguration() { + super(); + this.sparkMaster = "local[*]"; + this.sparkAppName = "Apache Hop"; + this.tempLocation = System.getProperty("java.io.tmpdir"); + this.genericTransformRunMode = SparkRunMode.DISTRIBUTED.name(); + setEnginePluginId(SparkConst.PLUGIN_ID); + setEnginePluginName(SparkConst.PLUGIN_NAME); + } + + public SparkPipelineRunConfiguration(SparkPipelineRunConfiguration config) { + super(config); + this.sparkMaster = config.sparkMaster; + this.sparkAppName = config.sparkAppName; + this.fatJar = config.fatJar; + this.sparkConfigs = config.sparkConfigs; + this.driverMemory = config.driverMemory; + this.executorMemory = config.executorMemory; + this.executorCores = config.executorCores; + this.tempLocation = config.tempLocation; + this.pluginsToStage = config.pluginsToStage; + this.pathSchemeMap = config.pathSchemeMap; + this.genericTransformRunMode = config.genericTransformRunMode; + } + + @Override + public SparkPipelineRunConfiguration clone() { + return new SparkPipelineRunConfiguration(this); + } + + /** Combo values for {@link #genericTransformRunMode}. */ + public List getGenericTransformRunModes( + ILogChannel log, IHopMetadataProvider metadataProvider) { + return SparkRunMode.configDisplayValues(); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/engines/template/SparkRunConfigTemplate.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/engines/template/SparkRunConfigTemplate.java new file mode 100644 index 00000000000..f6bb7cfb1ec --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/engines/template/SparkRunConfigTemplate.java @@ -0,0 +1,215 @@ +/* + * 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.hop.spark.engines.template; + +import java.util.Arrays; +import java.util.Objects; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.spark.engines.SparkPipelineRunConfiguration; + +/** + * Named presets for {@link SparkPipelineRunConfiguration} fields. Pure data — no SWT. Apply via + * {@link #applyTo(SparkPipelineRunConfiguration)}. + */ +public enum SparkRunConfigTemplate { + LOCAL_EXECUTION( + "Local execution", + "Embedded Spark in the Hop process using all local cores (local[*]).", + "local[*]", + "Apache Hop", + "", + "spark.ui.enabled=false", + "", + "", + ""), + LOCAL_CAPPED( + "Local (capped cores)", + "Embedded Spark limited to 4 local cores (local[4]).", + "local[4]", + "Apache Hop", + "", + "spark.ui.enabled=false", + "", + "", + ""), + SPARK_STANDALONE( + "Spark standalone server", + "Connect Hop as a Spark client to a standalone master (replace host).", + "spark://host:7077", + "Apache Hop", + "", + "spark.ui.enabled=false", + "2g", + "2g", + "2"), + SPARK_SUBMIT( + "spark-submit (cluster)", + "Leave master empty so spark-submit --master wins; use a native-provided fat jar.", + "", + "Apache Hop", + "", + "spark.ui.enabled=false", + "", + "", + ""), + DATABRICKS( + "Databricks", + "Cluster / submit oriented placeholders. Master empty for submit-style runs; set paths for your workspace.", + "", + "Apache Hop", + "", + """ +spark.ui.enabled=false +spark.executor.cores=3 +spark.driver.cores=1 +spark.default.parallelism=12 +spark.sql.shuffle.partitions=12 + """, + "", + "", + ""), + YARN_CLIENT( + "YARN client", + "Submit to YARN in client deploy mode (cluster provides Spark).", + "yarn", + "Apache Hop", + "", + "spark.ui.enabled=false\nspark.submit.deployMode=client", + "2g", + "2g", + "2"), + LAKEHOUSE_LOCAL( + "Lakehouse local", + "Local[*] for Delta/Iceberg pipelines (connectors on plugin classpath; session plan applies extensions).", + "local[*]", + "Apache Hop", + "", + "spark.ui.enabled=false", + "", + "", + ""); + + private final String displayName; + private final String description; + private final String sparkMaster; + private final String sparkAppName; + private final String fatJar; + private final String sparkConfigs; + private final String driverMemory; + private final String executorMemory; + private final String executorCores; + + SparkRunConfigTemplate( + String displayName, + String description, + String sparkMaster, + String sparkAppName, + String fatJar, + String sparkConfigs, + String driverMemory, + String executorMemory, + String executorCores) { + this.displayName = displayName; + this.description = description; + this.sparkMaster = sparkMaster; + this.sparkAppName = sparkAppName; + this.fatJar = fatJar; + this.sparkConfigs = sparkConfigs; + this.driverMemory = driverMemory; + this.executorMemory = executorMemory; + this.executorCores = executorCores; + } + + public String getDisplayName() { + return displayName; + } + + public String getDescription() { + return description; + } + + /** Labels for selection dialogs (same order as {@link #values()}). */ + public static String[] displayNames() { + return Arrays.stream(values()) + .map(SparkRunConfigTemplate::getDisplayName) + .toArray(String[]::new); + } + + public static SparkRunConfigTemplate fromDisplayName(String name) { + if (name == null) { + return null; + } + for (SparkRunConfigTemplate t : values()) { + if (t.displayName.equals(name)) { + return t; + } + } + return null; + } + + /** + * Apply this template's field values onto {@code config}. Does not change temp location or + * plugins to stage (keeps operator environment defaults). + */ + public void applyTo(SparkPipelineRunConfiguration config) { + Objects.requireNonNull(config, "config"); + config.setSparkMaster(sparkMaster); + config.setSparkAppName(sparkAppName); + config.setFatJar(fatJar); + config.setSparkConfigs(sparkConfigs); + config.setDriverMemory(driverMemory); + config.setExecutorMemory(executorMemory); + config.setExecutorCores(executorCores); + } + + /** + * True if the configuration looks customized relative to a fresh {@link + * SparkPipelineRunConfiguration} default (used for overwrite confirmation). + */ + public static boolean looksCustomized(SparkPipelineRunConfiguration config) { + if (config == null) { + return false; + } + SparkPipelineRunConfiguration def = new SparkPipelineRunConfiguration(); + if (!Objects.equals(nz(config.getSparkMaster()), nz(def.getSparkMaster()))) { + return true; + } + if (!Objects.equals(nz(config.getSparkAppName()), nz(def.getSparkAppName()))) { + return true; + } + if (StringUtils.isNotEmpty(config.getFatJar())) { + return true; + } + if (StringUtils.isNotEmpty(config.getSparkConfigs())) { + return true; + } + if (StringUtils.isNotEmpty(config.getDriverMemory()) + || StringUtils.isNotEmpty(config.getExecutorMemory()) + || StringUtils.isNotEmpty(config.getExecutorCores())) { + return true; + } + if (StringUtils.isNotEmpty(config.getPathSchemeMap())) { + return true; + } + return StringUtils.isNotEmpty(config.getPluginsToStage()); + } + + private static String nz(String s) { + return s == null ? "" : s; + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/execution/SparkTransformExecutionSampling.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/execution/SparkTransformExecutionSampling.java new file mode 100644 index 00000000000..3c6a2c717c2 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/execution/SparkTransformExecutionSampling.java @@ -0,0 +1,466 @@ +/* + * 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.hop.spark.execution; + +import com.fasterxml.jackson.core.JsonProcessingException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.Const; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.exception.HopRuntimeException; +import org.apache.hop.core.exception.HopTransformException; +import org.apache.hop.core.json.HopJson; +import org.apache.hop.core.logging.LogChannel; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.row.RowBuffer; +import org.apache.hop.core.row.RowMetaBuilder; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.execution.Execution; +import org.apache.hop.execution.ExecutionBuilder; +import org.apache.hop.execution.ExecutionDataBuilder; +import org.apache.hop.execution.ExecutionDataSetMeta; +import org.apache.hop.execution.ExecutionInfoLocation; +import org.apache.hop.execution.ExecutionState; +import org.apache.hop.execution.ExecutionStateBuilder; +import org.apache.hop.execution.ExecutionType; +import org.apache.hop.execution.IExecutionInfoLocation; +import org.apache.hop.execution.profiling.ExecutionDataProfile; +import org.apache.hop.execution.sampler.ExecutionDataSamplerMeta; +import org.apache.hop.execution.sampler.IExecutionDataSampler; +import org.apache.hop.execution.sampler.IExecutionDataSamplerStore; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.Pipeline; +import org.apache.hop.pipeline.config.PipelineRunConfiguration; +import org.apache.hop.pipeline.engine.IEngineComponent; +import org.apache.hop.pipeline.engines.local.LocalPipelineEngine; +import org.apache.hop.pipeline.transform.ITransform; +import org.apache.hop.pipeline.transform.RowAdapter; +import org.apache.hop.pipeline.transform.stream.IStream; +import org.apache.hop.spark.core.SparkExecutionDataAccumulator; + +/** + * Executor-side sampling for Hop transforms running in Spark mapPartitions, ported from Beam {@code + * TransformBaseFn}. + * + *

Samples are shipped to the driver via {@link SparkExecutionDataAccumulator} so the driver's + * execution info location (which owns the parent pipeline {@code CacheEntry}) can call {@code + * registerData}. Executor-local {@code registerData} is best-effort only (file locations). + * + *

Not serializable — construct after Hop/plugin init on the executor. + */ +@Getter +@Setter +public class SparkTransformExecutionSampling { + + private final String transformName; + private final String parentLogChannelId; + private final int copyNr; + private final String ownerLogChannelId; + + private ExecutionInfoLocation executionInfoLocation; + private List dataSamplers = List.of(); + private final List dataSamplerStores = new ArrayList<>(); + private Timer executionInfoTimer; + private LocalPipelineEngine pipeline; + private ITransform transform; + private boolean active; + private SparkExecutionDataAccumulator sampleDataAccumulator; + + public SparkTransformExecutionSampling( + String transformName, String parentLogChannelId, int copyNr) { + this(transformName, parentLogChannelId, copyNr, null); + } + + public SparkTransformExecutionSampling( + String transformName, + String parentLogChannelId, + int copyNr, + SparkExecutionDataAccumulator sampleDataAccumulator) { + this.transformName = transformName; + this.parentLogChannelId = parentLogChannelId; + this.copyNr = copyNr; + this.sampleDataAccumulator = sampleDataAccumulator; + String parent = + StringUtils.isNotEmpty(parentLogChannelId) ? parentLogChannelId : "spark-pipeline"; + this.ownerLogChannelId = parent + "|" + transformName + "|" + copyNr; + } + + /** + * Load location + data profile samplers from metadata. Sampling is only active when both an + * execution information location and a non-empty data profile (or extra samplers JSON) are + * present. + */ + @SuppressWarnings("rawtypes") + public void lookup( + IVariables variables, + IHopMetadataProvider metadataProvider, + String runConfigName, + String dataSamplersJson) + throws HopException, JsonProcessingException { + active = false; + executionInfoLocation = null; + dataSamplers = new ArrayList<>(); + dataSamplerStores.clear(); + + if (StringUtils.isEmpty(runConfigName) || metadataProvider == null) { + LogChannel.GENERAL.logDebug( + "Spark sampling inactive for '" + + transformName + + "': runConfigName or metadataProvider missing"); + return; + } + + PipelineRunConfiguration runConf = + metadataProvider.getSerializer(PipelineRunConfiguration.class).load(runConfigName); + if (runConf == null) { + LogChannel.GENERAL.logBasic( + "Spark sampling inactive for '" + + transformName + + "': run configuration '" + + runConfigName + + "' not found in metadata"); + return; + } + + String locationName = runConf.getExecutionInfoLocationName(); + if (StringUtils.isEmpty(locationName)) { + LogChannel.GENERAL.logDebug( + "Spark sampling inactive for '" + + transformName + + "': no execution info location on run configuration '" + + runConfigName + + "'"); + return; + } + + ExecutionInfoLocation location = + metadataProvider.getSerializer(ExecutionInfoLocation.class).load(locationName); + if (location == null) { + LogChannel.GENERAL.logBasic( + "Spark sampling inactive for '" + + transformName + + "': execution info location '" + + locationName + + "' not found in metadata"); + return; + } + + String profileName = runConf.getExecutionDataProfileName(); + if (StringUtils.isNotEmpty(profileName)) { + ExecutionDataProfile dataProfile = + metadataProvider.getSerializer(ExecutionDataProfile.class).load(profileName); + if (dataProfile != null && dataProfile.getSamplers() != null) { + dataSamplers.addAll(dataProfile.getSamplers()); + } else { + LogChannel.GENERAL.logBasic( + "Spark sampling: data profile '" + + profileName + + "' missing or has no samplers for transform '" + + transformName + + "'"); + } + } + + if (StringUtils.isNotEmpty(dataSamplersJson) && !"[]".equals(dataSamplersJson.trim())) { + try { + IExecutionDataSampler[] extraSamplers = + HopJson.newMapper().readValue(dataSamplersJson, IExecutionDataSampler[].class); + if (extraSamplers != null) { + dataSamplers.addAll(Arrays.asList(extraSamplers)); + } + } catch (LinkageError | Exception e) { + // local[*]: plugin CL may have a second Jackson → LinkageError on HopJson.newMapper(). + // Profile samplers above are enough; extra GUI samplers are best-effort. + LogChannel.GENERAL.logError( + "Unable to deserialize extra data samplers JSON for transform '" + + transformName + + "' (non-fatal): " + + e.getMessage()); + } + } + + if (dataSamplers.isEmpty()) { + LogChannel.GENERAL.logBasic( + "Spark sampling inactive for '" + + transformName + + "': no data samplers (profile='" + + Const.NVL(profileName, "") + + "', extraJson empty=" + + StringUtils.isEmpty(dataSamplersJson) + + ")"); + return; + } + + executionInfoLocation = location; + // Location init still needed for optional executor-local registerData (file locations) + IExecutionInfoLocation iLocation = executionInfoLocation.getExecutionInfoLocation(); + iLocation.initialize(variables, metadataProvider); + active = true; + LogChannel.GENERAL.logBasic( + "Spark sampling active for '" + + transformName + + "' copy " + + copyNr + + " (" + + dataSamplers.size() + + " sampler(s), location='" + + locationName + + "', driverAccumulator=" + + (sampleDataAccumulator != null) + + ")"); + } + + /** Register a transform execution node under the parent pipeline execution. */ + public void registerExecutingTransform(LocalPipelineEngine localPipeline) { + if (!active || executionInfoLocation == null || localPipeline == null) { + return; + } + try { + ITransform t = localPipeline.getTransform(transformName, 0); + if (t == null) { + return; + } + List copies = localPipeline.getComponentCopies(transformName); + if (copies == null || copies.isEmpty()) { + return; + } + IEngineComponent transformComponent = copies.get(0); + Execution execution = ExecutionBuilder.fromTransform(localPipeline, t).build(); + execution.setParentId(parentLogChannelId); + execution.setId(ownerLogChannelId); + execution.setCopyNr(Integer.toString(copyNr)); + executionInfoLocation.getExecutionInfoLocation().registerExecution(execution); + + ExecutionState transformState = + ExecutionStateBuilder.fromTransform(localPipeline, transformComponent).build(); + transformState.setParentId(parentLogChannelId); + transformState.setId(ownerLogChannelId); + transformState.setCopyNr(Integer.toString(copyNr)); + executionInfoLocation.getExecutionInfoLocation().updateExecutionState(transformState); + } catch (Exception e) { + LogChannel.GENERAL.logError( + "Error registering transform execution for sampling (non-fatal)", e); + } + } + + /** + * Attach profile samplers to transform OUTPUT rows and start a timer to flush samples to the + * location. + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + public void attach( + IVariables variables, + LocalPipelineEngine localPipeline, + ITransform transform, + IRowMeta inputRowMeta, + IRowMeta outputRowMeta) { + if (!active || executionInfoLocation == null || dataSamplers.isEmpty() || transform == null) { + return; + } + this.pipeline = localPipeline; + this.transform = transform; + + ExecutionDataSamplerMeta dataSamplerMeta = + new ExecutionDataSamplerMeta( + transformName, Integer.toString(copyNr), ownerLogChannelId, false, false); + + for (IExecutionDataSampler dataSampler : dataSamplers) { + IExecutionDataSamplerStore dataSamplerStore = dataSampler.createSamplerStore(dataSamplerMeta); + dataSamplerStore.init(variables, inputRowMeta, outputRowMeta); + dataSamplerStores.add(dataSamplerStore); + } + + transform.addRowListener( + new RowAdapter() { + @Override + public void rowWrittenEvent(IRowMeta rowMeta, Object[] row) throws HopTransformException { + for (int s = 0; s < dataSamplers.size(); s++) { + IExecutionDataSampler sampler = dataSamplers.get(s); + IExecutionDataSamplerStore store = dataSamplerStores.get(s); + try { + sampler.sampleRow(store, IStream.StreamType.OUTPUT, rowMeta, row); + } catch (HopException e) { + throw new HopRuntimeException("Error sampling row on Spark partition", e); + } + } + } + }); + + long delay = Const.toLong(executionInfoLocation.getDataLoggingDelay(), 5000L); + long interval = Const.toLong(executionInfoLocation.getDataLoggingInterval(), 10000L); + TimerTask task = + new TimerTask() { + @Override + public void run() { + try { + sendSamplesToLocation(false); + } catch (Exception e) { + LogChannel.GENERAL.logError( + "Error sending transform samples to location (non-fatal)", e); + } + } + }; + executionInfoTimer = new Timer("spark-sample-" + transformName + "-" + copyNr, true); + executionInfoTimer.schedule(task, delay, interval); + } + + /** + * Push sampler stores + transform metrics. Prefer the driver accumulator (reliable for caching + * locations); also best-effort {@code registerData} on this process for file locations. + */ + @SuppressWarnings("rawtypes") + public void sendSamplesToLocation(boolean finished) throws HopException { + if (!active || executionInfoLocation == null || pipeline == null || transform == null) { + return; + } + + ExecutionDataBuilder dataBuilder = + ExecutionDataBuilder.of() + .withOwnerId(ownerLogChannelId) + .withParentId(parentLogChannelId) + .withExecutionType(ExecutionType.Transform) + .withCollectionDate(new Date()) + .withFinished(finished); + + for (IExecutionDataSamplerStore store : dataSamplerStores) { + dataBuilder.addDataSets(store.getSamples()).addSetMeta(store.getSamplesMetadata()); + } + + dataBuilder.addSetMeta( + ownerLogChannelId, + new ExecutionDataSetMeta( + ownerLogChannelId, + ownerLogChannelId, + transformName, + ownerLogChannelId, + transformName + "." + ownerLogChannelId + " (Metrics)")); + dataBuilder.addDataSet( + ownerLogChannelId, + new RowBuffer( + new RowMetaBuilder().addString("metric").addInteger("value").build(), + List.of( + new Object[] {Pipeline.METRIC_NAME_INPUT, transform.getLinesInput()}, + new Object[] {Pipeline.METRIC_NAME_OUTPUT, transform.getLinesOutput()}, + new Object[] {Pipeline.METRIC_NAME_READ, transform.getLinesRead()}, + new Object[] {Pipeline.METRIC_NAME_WRITTEN, transform.getLinesWritten()}, + new Object[] {Pipeline.METRIC_NAME_REJECTED, transform.getLinesRejected()}, + new Object[] {Pipeline.METRIC_NAME_ERROR, transform.getErrors()}))); + + var executionData = dataBuilder.build(); + + // Primary path: ship JSON to the driver (owns parent CacheEntry for caching locations) + if (sampleDataAccumulator != null) { + try { + String json = HopJson.newMapper().writeValueAsString(executionData); + sampleDataAccumulator.addSample(ownerLogChannelId, json); + } catch (LinkageError e) { + // Dual Jackson on local[*] plugin CL — fall through to local registerData only + LogChannel.GENERAL.logError( + "Unable to serialize samples via HopJson for '" + + transformName + + "' (using local registerData only): " + + e.getMessage()); + } catch (JsonProcessingException e) { + throw new HopException( + "Unable to serialize execution sample data for transform '" + transformName + "'", e); + } + } + + // Best-effort local registration (works for file locations when parent folder exists) + IExecutionInfoLocation iLocation = executionInfoLocation.getExecutionInfoLocation(); + try { + iLocation.registerData(executionData); + } catch (Exception e) { + if (sampleDataAccumulator == null) { + throw e instanceof HopException hopEx + ? hopEx + : new HopException("Error registering execution data locally", e); + } + LogChannel.GENERAL.logDebug( + "Local registerData skipped/failed for '" + + transformName + + "' (samples sent via driver accumulator): " + + e.getMessage()); + } + + List copies = pipeline.getComponentCopies(transformName); + if (copies != null && !copies.isEmpty()) { + ExecutionState transformState = + ExecutionStateBuilder.fromTransform(pipeline, copies.get(0)).build(); + transformState.setParentId(parentLogChannelId); + transformState.setId(ownerLogChannelId); + transformState.setCopyNr(Integer.toString(copyNr)); + try { + iLocation.updateExecutionState(transformState); + } catch (Exception e) { + LogChannel.GENERAL.logDebug( + "Local updateExecutionState skipped for '" + transformName + "': " + e.getMessage()); + } + } + } + + /** Final sample flush, cancel timer, close location instance for this partition. */ + public void close() { + if (!active) { + return; + } + try { + if (executionInfoTimer != null) { + executionInfoTimer.cancel(); + executionInfoTimer = null; + } + try { + sendSamplesToLocation(true); + } catch (Exception e) { + LogChannel.GENERAL.logError("Error sending final transform samples (non-fatal)", e); + } + if (executionInfoLocation != null + && executionInfoLocation.getExecutionInfoLocation() != null) { + try { + executionInfoLocation.getExecutionInfoLocation().close(); + } catch (Exception e) { + LogChannel.GENERAL.logError( + "Error closing execution info location on partition (non-fatal)", e); + } + } + } finally { + active = false; + } + } + + /** Serialize extra GUI samplers for the mapPartitions closure (Beam-compatible). */ + public static String serializeDataSamplers( + List> dataSamplers) + throws HopException { + try { + if (dataSamplers == null || dataSamplers.isEmpty()) { + return "[]"; + } + return HopJson.newMapper().writeValueAsString(dataSamplers); + } catch (JsonProcessingException e) { + throw new HopException("Unable to serialize execution data samplers", e); + } + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/gui/DrawSparkRunModeOnTransformExtensionPoint.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/gui/DrawSparkRunModeOnTransformExtensionPoint.java new file mode 100644 index 00000000000..c360d185c29 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/gui/DrawSparkRunModeOnTransformExtensionPoint.java @@ -0,0 +1,100 @@ +/* + * 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.hop.spark.gui; + +import org.apache.hop.core.extension.ExtensionPoint; +import org.apache.hop.core.extension.IExtensionPoint; +import org.apache.hop.core.gui.AreaOwner; +import org.apache.hop.core.gui.IGc; +import org.apache.hop.core.gui.Rectangle; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.svg.SvgFile; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.pipeline.PipelinePainterExtension; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.util.SparkRunMode; + +/** + * Draws a small badge at the bottom-left of a transform icon when a Spark run-mode override is set + * (Force distributed / Force Driver Only). + */ +@ExtensionPoint( + id = "DrawSparkRunModeOnTransformExtensionPoint", + description = + "Draw Spark run-mode override badge (bottom-left) on transforms with Force Distributed or Force Driver Only", + extensionPointId = "PipelinePainterTransform") +public class DrawSparkRunModeOnTransformExtensionPoint + implements IExtensionPoint { + + public static final String AREA_OWNER_PREFIX = "Spark run mode: "; + + @Override + public void callExtensionPoint( + ILogChannel log, IVariables variables, PipelinePainterExtension ext) { + try { + TransformMeta transformMeta = ext.transformMeta; + if (transformMeta == null || !SparkRunMode.hasForcedOverride(transformMeta)) { + return; + } + + String override = SparkRunMode.getOverride(transformMeta); + String svgName = + SparkRunMode.OVERRIDE_FORCE_DRIVER_ONLY.equals(override) + ? "spark-run-driver.svg" + : "spark-run-distributed.svg"; + String label = SparkRunMode.displayLabelForOverride(override); + + Rectangle r = + drawBadge( + ext.gc, ext.x1, ext.y1, ext.iconSize, svgName, this.getClass().getClassLoader()); + ext.areaOwners.add( + new AreaOwner( + AreaOwner.AreaType.CUSTOM, + r.x, + r.y, + r.width, + r.height, + ext.offset, + transformMeta, + AREA_OWNER_PREFIX + label)); + } catch (Exception e) { + // Not critical for canvas painting + log.logError("Error drawing Spark run-mode badge", e); + } + } + + /** Bottom-left corner of the transform icon (badge overlaps the lower-left of the glyph). */ + static Rectangle drawBadge( + IGc gc, int x, int y, int iconSize, String svgResource, ClassLoader classLoader) + throws Exception { + int imageWidth = 16; + int imageHeight = 16; + int locationX = x - (2 * imageWidth) / 3; + int locationY = y + iconSize - imageHeight / 3; + + gc.drawImage( + new SvgFile(svgResource, classLoader), + locationX, + locationY, + imageWidth, + imageHeight, + gc.getMagnification(), + 0); + return new Rectangle(locationX, locationY, imageWidth, imageHeight); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/gui/HopSparkGuiPlugin.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/gui/HopSparkGuiPlugin.java new file mode 100644 index 00000000000..87e1335c45d --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/gui/HopSparkGuiPlugin.java @@ -0,0 +1,232 @@ +/* + * 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.hop.spark.gui; + +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.Const; +import org.apache.hop.core.action.GuiContextAction; +import org.apache.hop.core.gui.plugin.GuiPlugin; +import org.apache.hop.core.gui.plugin.action.GuiActionType; +import org.apache.hop.core.gui.plugin.menu.GuiMenuElement; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.i18n.BaseMessages; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.pkg.SparkProjectPackage; +import org.apache.hop.spark.util.SparkRunMode; +import org.apache.hop.ui.core.dialog.BaseDialog; +import org.apache.hop.ui.core.dialog.EnterSelectionDialog; +import org.apache.hop.ui.core.dialog.ErrorDialog; +import org.apache.hop.ui.core.gui.GuiResource; +import org.apache.hop.ui.hopgui.HopGui; +import org.apache.hop.ui.hopgui.file.pipeline.context.HopGuiPipelineTransformContext; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.MessageBox; +import org.eclipse.swt.widgets.Shell; + +/** + * GUI actions for the Native Spark engine, including project package export for cluster execution + * and per-transform Spark run-mode overrides. + */ +@GuiPlugin +public class HopSparkGuiPlugin { + + public static final Class PKG = HopSparkGuiPlugin.class; + + public static final String ID_MAIN_MENU_TOOLS_EXPORT_SPARK_PROJECT = + "40300-menu-tools-export-spark-project-package"; + + public static final String ID_MAIN_MENU_TOOLS_SPARK_PACKAGE_CONFIG = + "40310-menu-tools-spark-package-config"; + + public static final String ACTION_ID_PIPELINE_GRAPH_TRANSFORM_SPARK_RUN_MODE = + "pipeline-graph-transform-spark-run-mode"; + + private static HopSparkGuiPlugin instance; + + public static HopSparkGuiPlugin getInstance() { + if (instance == null) { + instance = new HopSparkGuiPlugin(); + } + return instance; + } + + /** + * Per-transform override of generic mapPartitions run mode (Inherit / Force distributed / Force + * Driver Only). Stored on {@link TransformMeta} attributes; effective when running on Native + * Spark. + */ + @GuiContextAction( + id = ACTION_ID_PIPELINE_GRAPH_TRANSFORM_SPARK_RUN_MODE, + parentId = HopGuiPipelineTransformContext.CONTEXT_ID, + type = GuiActionType.Modify, + name = "i18n::SparkGuiPlugin.ContextAction.SparkRunMode.Name", + tooltip = "i18n::SparkGuiPlugin.ContextAction.SparkRunMode.Tooltip", + image = "spark-run-driver.svg", + category = "i18n::SparkGuiPlugin.ContextAction.Category", + categoryOrder = "9") + public void setSparkRunMode(HopGuiPipelineTransformContext context) { + HopGui hopGui = HopGui.getInstance(); + try { + TransformMeta transformMeta = context.getTransformMeta(); + PipelineMeta pipelineMeta = context.getPipelineMeta(); + + String currentLabel = + SparkRunMode.displayLabelForOverride(SparkRunMode.getOverride(transformMeta)); + String[] labels = SparkRunMode.overrideDisplayLabels(); + int preselect = 0; + for (int i = 0; i < labels.length; i++) { + if (labels[i].equals(currentLabel)) { + preselect = i; + break; + } + } + EnterSelectionDialog dialog = + new EnterSelectionDialog( + hopGui.getShell(), + labels, + BaseMessages.getString(PKG, "SparkGuiPlugin.ContextAction.SparkRunMode.Dialog.Title"), + BaseMessages.getString( + PKG, + "SparkGuiPlugin.ContextAction.SparkRunMode.Dialog.Message", + transformMeta.getName(), + currentLabel)); + dialog.setAvoidQuickSearch(); + String choice = dialog.open(preselect); + if (choice == null) { + return; + } + SparkRunMode.setOverride(transformMeta, SparkRunMode.overrideFromDisplayLabel(choice)); + transformMeta.setChanged(); + pipelineMeta.setChanged(); + context.getPipelineGraph().updateGui(); + } catch (Exception e) { + new ErrorDialog( + hopGui.getShell(), + BaseMessages.getString(PKG, "SparkGuiPlugin.ContextAction.SparkRunMode.Error.Header"), + BaseMessages.getString(PKG, "SparkGuiPlugin.ContextAction.SparkRunMode.Error.Message"), + e); + } + } + + /** Edit {@code spark-package.json} include/exclude rules for Native Spark project packages. */ + @GuiMenuElement( + root = HopGui.ID_MAIN_MENU, + id = ID_MAIN_MENU_TOOLS_SPARK_PACKAGE_CONFIG, + label = "i18n::SparkGuiPlugin.Menu.SparkPackageConfig.Text", + parentId = HopGui.ID_MAIN_MENU_TOOLS_PARENT_ID, + image = "spark-file-input.svg", + separator = true) + public void menuToolsSparkPackageConfig() { + HopGui hopGui = HopGui.getInstance(); + Shell shell = hopGui.getShell(); + IVariables variables = hopGui.getVariables(); + String projectHome = variables.getVariable("PROJECT_HOME"); + if (StringUtils.isEmpty(projectHome)) { + MessageBox box = new MessageBox(shell, SWT.CLOSE | SWT.ICON_WARNING); + box.setText( + BaseMessages.getString(PKG, "SparkGuiPlugin.ExportSparkProject.NoProject.Header")); + box.setMessage( + BaseMessages.getString(PKG, "SparkGuiPlugin.ExportSparkProject.NoProject.Message")); + box.open(); + return; + } + projectHome = variables.resolve(projectHome); + new SparkPackageConfigDialog(shell, projectHome).open(); + } + + /** + * Export the active project as a zip for Native Spark execution (MainSpark + * {@code --HopProjectPackage}). Not the same as File → Export current project to zip. + */ + @GuiMenuElement( + root = HopGui.ID_MAIN_MENU, + id = ID_MAIN_MENU_TOOLS_EXPORT_SPARK_PROJECT, + label = "i18n::SparkGuiPlugin.Menu.ExportSparkProjectPackage.Text", + parentId = HopGui.ID_MAIN_MENU_TOOLS_PARENT_ID, + image = "spark-file-input.svg") + public void menuToolsExportSparkProjectPackage() { + HopGui hopGui = HopGui.getInstance(); + Shell shell = hopGui.getShell(); + IVariables variables = hopGui.getVariables(); + + MessageBox intro = new MessageBox(shell, SWT.OK | SWT.CANCEL | SWT.ICON_INFORMATION); + intro.setText(BaseMessages.getString(PKG, "SparkGuiPlugin.ExportSparkProject.Dialog.Header")); + intro.setMessage( + BaseMessages.getString(PKG, "SparkGuiPlugin.ExportSparkProject.Dialog.Message1") + + Const.CR + + Const.CR + + BaseMessages.getString(PKG, "SparkGuiPlugin.ExportSparkProject.Dialog.Message2") + + Const.CR + + Const.CR + + BaseMessages.getString(PKG, "SparkGuiPlugin.ExportSparkProject.Dialog.Message3")); + if ((intro.open() & SWT.CANCEL) != 0) { + return; + } + + String projectHome = variables.getVariable("PROJECT_HOME"); + if (StringUtils.isEmpty(projectHome)) { + MessageBox box = new MessageBox(shell, SWT.CLOSE | SWT.ICON_WARNING); + box.setText( + BaseMessages.getString(PKG, "SparkGuiPlugin.ExportSparkProject.NoProject.Header")); + box.setMessage( + BaseMessages.getString(PKG, "SparkGuiPlugin.ExportSparkProject.NoProject.Message")); + box.open(); + return; + } + + String zipFilename = + BaseDialog.presentFileDialog( + true, + shell, + new String[] {"*.zip", "*.*"}, + new String[] { + BaseMessages.getString(PKG, "SparkGuiPlugin.FileTypes.Zip.Label"), + BaseMessages.getString(PKG, "SparkGuiPlugin.FileTypes.All.Label") + }, + true); + if (zipFilename == null) { + return; + } + + try { + SparkProjectPackage.exportProject( + projectHome, zipFilename, hopGui.getMetadataProvider(), variables); + + GuiResource.getInstance().toClipboard(zipFilename); + + MessageBox done = new MessageBox(shell, SWT.CLOSE | SWT.ICON_INFORMATION); + done.setText(BaseMessages.getString(PKG, "SparkGuiPlugin.ExportSparkProject.Done.Header")); + done.setMessage( + BaseMessages.getString( + PKG, "SparkGuiPlugin.ExportSparkProject.Done.Message1", zipFilename) + + Const.CR + + Const.CR + + BaseMessages.getString(PKG, "SparkGuiPlugin.ExportSparkProject.Done.Message2") + + Const.CR + + BaseMessages.getString(PKG, "SparkGuiPlugin.ExportSparkProject.Done.Message3")); + done.open(); + } catch (Exception e) { + new ErrorDialog( + shell, + BaseMessages.getString(PKG, "SparkGuiPlugin.ExportSparkProject.Error.Header"), + BaseMessages.getString(PKG, "SparkGuiPlugin.ExportSparkProject.Error.Message"), + e); + } + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/gui/SparkPackageConfigDialog.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/gui/SparkPackageConfigDialog.java new file mode 100644 index 00000000000..626ebb572cb --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/gui/SparkPackageConfigDialog.java @@ -0,0 +1,261 @@ +/* + * 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.hop.spark.gui; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.Const; +import org.apache.hop.i18n.BaseMessages; +import org.apache.hop.spark.pkg.PackageExportFilter; +import org.apache.hop.ui.core.PropsUi; +import org.apache.hop.ui.core.dialog.BaseDialog; +import org.apache.hop.ui.core.dialog.ErrorDialog; +import org.apache.hop.ui.core.gui.WindowProperty; +import org.apache.hop.ui.pipeline.transform.BaseTransformDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +/** + * Edit {@link PackageExportFilter#CONFIG_FILENAME} for the active project (Native Spark package + * include/exclude rules). + */ +public class SparkPackageConfigDialog extends Dialog { + private static final Class PKG = HopSparkGuiPlugin.class; + + private final String projectHome; + private Shell shell; + private PropsUi props; + + private Text wProjectHome; + private StyledText wExcludeDirs; + private StyledText wExcludeGlobs; + private StyledText wIncludePaths; + private Button wReplaceDefaults; + + public SparkPackageConfigDialog(Shell parent, String projectHome) { + super(parent, SWT.NONE); + this.projectHome = projectHome; + this.props = PropsUi.getInstance(); + } + + public void open() { + Shell parent = getParent(); + + shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.RESIZE | SWT.MAX | SWT.MIN); + props.setLook(shell); + shell.setText(BaseMessages.getString(PKG, "SparkPackageConfigDialog.Shell.Title")); + + FormLayout formLayout = new FormLayout(); + formLayout.marginWidth = PropsUi.getFormMargin(); + formLayout.marginHeight = PropsUi.getFormMargin(); + shell.setLayout(formLayout); + + int middle = props.getMiddlePct(); + int margin = PropsUi.getMargin(); + + Button wOk = new Button(shell, SWT.PUSH); + wOk.setText(BaseMessages.getString(PKG, "System.Button.OK")); + Button wCancel = new Button(shell, SWT.PUSH); + wCancel.setText(BaseMessages.getString(PKG, "System.Button.Cancel")); + BaseTransformDialog.positionBottomButtons(shell, new Button[] {wOk, wCancel}, margin, null); + wOk.addListener(SWT.Selection, e -> ok()); + wCancel.addListener(SWT.Selection, e -> cancel()); + + Label wlIntro = new Label(shell, SWT.LEFT | SWT.WRAP); + props.setLook(wlIntro); + wlIntro.setText(BaseMessages.getString(PKG, "SparkPackageConfigDialog.Intro")); + FormData fdlIntro = new FormData(); + fdlIntro.left = new FormAttachment(0, 0); + fdlIntro.top = new FormAttachment(0, margin); + fdlIntro.right = new FormAttachment(100, 0); + wlIntro.setLayoutData(fdlIntro); + + Label wlHome = new Label(shell, SWT.RIGHT); + props.setLook(wlHome); + wlHome.setText(BaseMessages.getString(PKG, "SparkPackageConfigDialog.ProjectHome.Label")); + FormData fdlHome = new FormData(); + fdlHome.left = new FormAttachment(0, 0); + fdlHome.top = new FormAttachment(wlIntro, margin * 2); + fdlHome.right = new FormAttachment(middle, -margin); + wlHome.setLayoutData(fdlHome); + wProjectHome = new Text(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER | SWT.READ_ONLY); + props.setLook(wProjectHome); + FormData fdHome = new FormData(); + fdHome.left = new FormAttachment(middle, 0); + fdHome.top = new FormAttachment(wlIntro, margin * 2); + fdHome.right = new FormAttachment(100, 0); + wProjectHome.setLayoutData(fdHome); + + Label wlDefaults = new Label(shell, SWT.LEFT | SWT.WRAP); + props.setLook(wlDefaults); + wlDefaults.setText( + BaseMessages.getString( + PKG, + "SparkPackageConfigDialog.Defaults.Label", + String.join(", ", PackageExportFilter.DEFAULT_EXCLUDE_DIRS))); + FormData fdlDefaults = new FormData(); + fdlDefaults.left = new FormAttachment(0, 0); + fdlDefaults.top = new FormAttachment(wProjectHome, margin); + fdlDefaults.right = new FormAttachment(100, 0); + wlDefaults.setLayoutData(fdlDefaults); + + wReplaceDefaults = new Button(shell, SWT.CHECK); + props.setLook(wReplaceDefaults); + wReplaceDefaults.setText( + BaseMessages.getString(PKG, "SparkPackageConfigDialog.ReplaceDefaults.Label")); + FormData fdReplace = new FormData(); + fdReplace.left = new FormAttachment(middle, 0); + fdReplace.top = new FormAttachment(wlDefaults, margin); + fdReplace.right = new FormAttachment(100, 0); + wReplaceDefaults.setLayoutData(fdReplace); + + wExcludeDirs = + addMultiLine( + shell, + wReplaceDefaults, + middle, + margin, + "SparkPackageConfigDialog.ExcludeDirs.Label", + "SparkPackageConfigDialog.ExcludeDirs.Tooltip", + 60); + wExcludeGlobs = + addMultiLine( + shell, + wExcludeDirs, + middle, + margin, + "SparkPackageConfigDialog.ExcludeGlobs.Label", + "SparkPackageConfigDialog.ExcludeGlobs.Tooltip", + 60); + wIncludePaths = + addMultiLine( + shell, + wExcludeGlobs, + middle, + margin, + "SparkPackageConfigDialog.IncludePaths.Label", + "SparkPackageConfigDialog.IncludePaths.Tooltip", + 80); + + // stretch last field to bottom buttons + FormData fdInc = (FormData) wIncludePaths.getLayoutData(); + fdInc.bottom = new FormAttachment(wOk, -margin); + + getData(); + // Blocks until the shell is disposed (OK / Cancel / Escape / window close). + // Do not add listeners or run another event loop after this returns — the shell is gone. + BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel()); + } + + private StyledText addMultiLine( + Shell shell, + org.eclipse.swt.widgets.Control above, + int middle, + int margin, + String labelKey, + String tooltipKey, + int height) { + Label wl = new Label(shell, SWT.RIGHT); + props.setLook(wl); + wl.setText(BaseMessages.getString(PKG, labelKey)); + FormData fdl = new FormData(); + fdl.left = new FormAttachment(0, 0); + fdl.top = new FormAttachment(above, margin); + fdl.right = new FormAttachment(middle, -margin); + wl.setLayoutData(fdl); + StyledText w = + new StyledText(shell, SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL); + props.setLook(w); + w.setToolTipText(BaseMessages.getString(PKG, tooltipKey)); + FormData fd = new FormData(); + fd.left = new FormAttachment(middle, 0); + fd.top = new FormAttachment(above, margin); + fd.right = new FormAttachment(100, 0); + fd.height = height; + w.setLayoutData(fd); + return w; + } + + private void getData() { + wProjectHome.setText(Const.NVL(projectHome, "")); + try { + PackageExportFilter filter = PackageExportFilter.loadFromProjectHome(projectHome); + wExcludeDirs.setText(String.join(Const.CR, filter.getExcludeDirs())); + wExcludeGlobs.setText(String.join(Const.CR, filter.getExcludeGlobs())); + wIncludePaths.setText(String.join(Const.CR, filter.getIncludePaths())); + wReplaceDefaults.setSelection(filter.isReplaceDefaultExcludeDirs()); + } catch (Exception e) { + new ErrorDialog( + shell, + BaseMessages.getString(PKG, "SparkPackageConfigDialog.Error.Load.Title"), + BaseMessages.getString(PKG, "SparkPackageConfigDialog.Error.Load.Message"), + e); + } + } + + private void ok() { + try { + PackageExportFilter filter = + new PackageExportFilter( + linesToList(wExcludeDirs.getText()), + linesToList(wExcludeGlobs.getText()), + linesToList(wIncludePaths.getText()), + wReplaceDefaults.getSelection()); + PackageExportFilter.saveToProjectHome(projectHome, filter); + dispose(); + } catch (Exception e) { + new ErrorDialog( + shell, + BaseMessages.getString(PKG, "SparkPackageConfigDialog.Error.Save.Title"), + BaseMessages.getString(PKG, "SparkPackageConfigDialog.Error.Save.Message"), + e); + } + } + + private static List linesToList(String text) { + if (StringUtils.isBlank(text)) { + return List.of(); + } + return Arrays.stream(text.split("\\R")) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toList()); + } + + private void cancel() { + dispose(); + } + + private void dispose() { + if (shell == null || shell.isDisposed()) { + return; + } + props.setScreen(new WindowProperty(shell)); + shell.dispose(); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/metadata/SparkCatalog.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/metadata/SparkCatalog.java new file mode 100644 index 00000000000..13c22a88b17 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/metadata/SparkCatalog.java @@ -0,0 +1,192 @@ +/* + * 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.hop.spark.metadata; + +import java.io.Serializable; +import lombok.Getter; +import lombok.Setter; +import org.apache.hop.core.gui.plugin.GuiElementType; +import org.apache.hop.core.gui.plugin.GuiPlugin; +import org.apache.hop.core.gui.plugin.GuiWidgetElement; +import org.apache.hop.i18n.BaseMessages; +import org.apache.hop.metadata.api.HopMetadata; +import org.apache.hop.metadata.api.HopMetadataBase; +import org.apache.hop.metadata.api.HopMetadataCategory; +import org.apache.hop.metadata.api.HopMetadataProperty; +import org.apache.hop.metadata.api.IHopMetadata; +import org.apache.hop.spark.metadata.template.SparkCatalogTemplate; +import org.apache.hop.spark.table.SparkLakeFormats; +import org.apache.hop.ui.core.dialog.EnterSelectionDialog; +import org.apache.hop.ui.hopgui.HopGui; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.MessageBox; +import org.eclipse.swt.widgets.Shell; + +/** + * Reusable Spark SQL catalog configuration for lakehouse TABLE mode (Iceberg Hadoop / REST, etc.). + * Applied as {@code spark.sql.catalog..*} when building a native Spark session. + */ +@Getter +@Setter +@GuiPlugin +@HopMetadata( + key = "SparkCatalog", + name = "i18n::SparkCatalog.Name", + description = "i18n::SparkCatalog.Description", + image = "spark-catalog.svg", + category = HopMetadataCategory.CONNECTIONS, + documentationUrl = "/metadata-types/spark-catalog.html") +public class SparkCatalog extends HopMetadataBase implements Serializable, IHopMetadata { + + private static final Class PKG = SparkCatalog.class; + + public static final String TYPE_HADOOP = "hadoop"; + public static final String TYPE_REST = "rest"; + public static final String TYPE_CUSTOM = "custom"; + + /** Advanced — confExtra only; not packaged by default. */ + public static final String TYPE_HIVE = "hive"; + + /** Advanced — confExtra only; not packaged by default. */ + public static final String TYPE_GLUE = "glue"; + + private static final String PARENT = SparkCatalogEditor.GUI_WIDGETS_PARENT_ID; + + /** + * Fill catalog fields from a named scenario (Iceberg Hadoop/REST, Hive, Glue, …). Mutates {@code + * object} (the live editor model); widgets are refreshed by GuiCompositeWidgets. + */ + @GuiWidgetElement( + id = "load-spark-catalog-template", + order = "09990-catalog-template", + parentId = PARENT, + type = GuiElementType.BUTTON, + label = "i18n::SparkCatalog.LoadTemplate.Label", + toolTip = "i18n::SparkCatalog.LoadTemplate.ToolTip") + public void loadCatalogTemplate(Object object) { + if (!(object instanceof SparkCatalog catalog)) { + return; + } + Shell shell = HopGui.getInstance().getShell(); + String[] labels = SparkCatalogTemplate.displayNames(); + EnterSelectionDialog dialog = + new EnterSelectionDialog( + shell, + labels, + BaseMessages.getString(PKG, "SparkCatalog.LoadTemplate.Dialog.Title"), + BaseMessages.getString(PKG, "SparkCatalog.LoadTemplate.Dialog.Message")); + dialog.setAvoidQuickSearch(); + String choice = dialog.open(); + if (choice == null) { + return; + } + SparkCatalogTemplate template = SparkCatalogTemplate.fromDisplayName(choice); + if (template == null) { + return; + } + if (SparkCatalogTemplate.looksCustomized(catalog)) { + MessageBox box = new MessageBox(shell, SWT.YES | SWT.NO | SWT.ICON_QUESTION); + box.setText(BaseMessages.getString(PKG, "SparkCatalog.LoadTemplate.Confirm.Title")); + box.setMessage( + BaseMessages.getString( + PKG, "SparkCatalog.LoadTemplate.Confirm.Message", template.getDisplayName())); + if (box.open() != SWT.YES) { + return; + } + } + template.applyTo(catalog); + } + + @GuiWidgetElement( + id = "10000-catalog-name", + parentId = PARENT, + type = GuiElementType.TEXT, + label = "i18n::SparkCatalog.CatalogName.Label", + toolTip = "i18n::SparkCatalog.CatalogName.Tooltip") + @HopMetadataProperty + private String catalogName; + + @GuiWidgetElement( + id = "10010-catalog-type", + parentId = PARENT, + type = GuiElementType.COMBO, + label = "i18n::SparkCatalog.CatalogType.Label", + toolTip = "i18n::SparkCatalog.CatalogType.Tooltip", + comboValuesMethod = "getCatalogTypeValues") + @HopMetadataProperty + private String catalogType = TYPE_HADOOP; + + @GuiWidgetElement( + id = "10020-implementation", + parentId = PARENT, + type = GuiElementType.TEXT, + label = "i18n::SparkCatalog.Implementation.Label", + toolTip = "i18n::SparkCatalog.Implementation.Tooltip") + @HopMetadataProperty + private String implementation; + + @GuiWidgetElement( + id = "10030-warehouse", + parentId = PARENT, + type = GuiElementType.TEXT, + label = "i18n::SparkCatalog.Warehouse.Label", + toolTip = "i18n::SparkCatalog.Warehouse.Tooltip") + @HopMetadataProperty + private String warehouse; + + @GuiWidgetElement( + id = "10040-uri", + parentId = PARENT, + type = GuiElementType.TEXT, + label = "i18n::SparkCatalog.Uri.Label", + toolTip = "i18n::SparkCatalog.Uri.Tooltip") + @HopMetadataProperty + private String uri; + + @GuiWidgetElement( + id = "10050-credential", + parentId = PARENT, + type = GuiElementType.TEXT, + password = true, + label = "i18n::SparkCatalog.Credential.Label", + toolTip = "i18n::SparkCatalog.Credential.Tooltip") + @HopMetadataProperty(password = true) + private String credential; + + @GuiWidgetElement( + id = "10060-conf-extra", + parentId = PARENT, + type = GuiElementType.MULTI_LINE_TEXT, + multiLineTextHeight = 5, + label = "i18n::SparkCatalog.ConfExtra.Label", + toolTip = "i18n::SparkCatalog.ConfExtra.Tooltip") + @HopMetadataProperty + private String confExtra; + + public SparkCatalog() { + this.catalogType = TYPE_HADOOP; + this.implementation = SparkLakeFormats.ICEBERG_CATALOG; + } + + /** Combo values for catalog type widget (GuiCompositeWidgets signature). */ + public java.util.List getCatalogTypeValues( + org.apache.hop.core.logging.ILogChannel log, + org.apache.hop.metadata.api.IHopMetadataProvider metadataProvider) { + return java.util.List.of(TYPE_HADOOP, TYPE_REST, TYPE_CUSTOM, TYPE_HIVE, TYPE_GLUE); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/metadata/SparkCatalogEditor.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/metadata/SparkCatalogEditor.java new file mode 100644 index 00000000000..c15e970f9c3 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/metadata/SparkCatalogEditor.java @@ -0,0 +1,156 @@ +/* + * 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.hop.spark.metadata; + +import org.apache.hop.core.Const; +import org.apache.hop.core.gui.plugin.GuiPlugin; +import org.apache.hop.i18n.BaseMessages; +import org.apache.hop.ui.core.PropsUi; +import org.apache.hop.ui.core.gui.GuiCompositeWidgets; +import org.apache.hop.ui.core.gui.GuiCompositeWidgetsAdapter; +import org.apache.hop.ui.core.gui.IGuiPluginCompositeButtonsListener; +import org.apache.hop.ui.core.metadata.MetadataEditor; +import org.apache.hop.ui.core.metadata.MetadataManager; +import org.apache.hop.ui.hopgui.HopGui; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +@GuiPlugin(description = "Editor for Spark catalog metadata") +public class SparkCatalogEditor extends MetadataEditor { + + private static final Class PKG = SparkCatalog.class; + + public static final String GUI_WIDGETS_PARENT_ID = "SparkCatalogEditor-GuiWidgetsParent"; + + private Text wName; + private Composite wWidgetsComposite; + private GuiCompositeWidgets guiCompositeWidgets; + + public SparkCatalogEditor( + HopGui hopGui, MetadataManager manager, SparkCatalog metadata) { + super(hopGui, manager, metadata); + } + + @Override + public void createControl(Composite parent) { + PropsUi props = PropsUi.getInstance(); + int middle = props.getMiddlePct(); + int margin = PropsUi.getMargin() + 2; + + Label wIcon = new Label(parent, SWT.RIGHT); + wIcon.setImage(getImage()); + FormData fdlIcon = new FormData(); + fdlIcon.top = new FormAttachment(0, 0); + fdlIcon.right = new FormAttachment(100, 0); + wIcon.setLayoutData(fdlIcon); + PropsUi.setLook(wIcon); + + Label wlName = new Label(parent, SWT.RIGHT); + PropsUi.setLook(wlName); + wlName.setText(BaseMessages.getString(PKG, "SparkCatalog.Name.Label")); + FormData fdlName = new FormData(); + fdlName.top = new FormAttachment(0, margin); + fdlName.left = new FormAttachment(0, 0); + fdlName.right = new FormAttachment(middle, -margin); + wlName.setLayoutData(fdlName); + wName = new Text(parent, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wName); + FormData fdName = new FormData(); + fdName.top = new FormAttachment(wlName, 0, SWT.CENTER); + fdName.left = new FormAttachment(middle, 0); + fdName.right = new FormAttachment(wIcon, -margin); + wName.setLayoutData(fdName); + Control lastControl = wName; + + wWidgetsComposite = new Composite(parent, SWT.NONE); + PropsUi.setLook(wWidgetsComposite); + wWidgetsComposite.setLayout(new FormLayout()); + FormData fdWidgetsComposite = new FormData(); + fdWidgetsComposite.top = new FormAttachment(lastControl, margin); + fdWidgetsComposite.left = new FormAttachment(0, 0); + fdWidgetsComposite.right = new FormAttachment(100, 0); + fdWidgetsComposite.bottom = new FormAttachment(100, 0); + wWidgetsComposite.setLayoutData(fdWidgetsComposite); + + guiCompositeWidgets = new GuiCompositeWidgets(manager.getVariables()); + guiCompositeWidgets.createCompositeWidgets( + metadata, null, wWidgetsComposite, GUI_WIDGETS_PARENT_ID, lastControl); + guiCompositeWidgets.setWidgetsListener( + new GuiCompositeWidgetsAdapter() { + @Override + public void widgetModified( + GuiCompositeWidgets compositeWidgets, Control changedWidget, String widgetId) { + setChanged(); + } + }); + // Flush form → model before button methods; re-bind model → form after mutations + // (e.g. Load catalog template). Do not touch wName (Hop metadata name stays independent). + guiCompositeWidgets.setCompositeButtonsListener( + new IGuiPluginCompositeButtonsListener() { + @Override + public void buttonPressed(Object sourceObject) { + Object model = sourceObject != null ? sourceObject : metadata; + guiCompositeWidgets.getWidgetsContents(model, GUI_WIDGETS_PARENT_ID); + } + + @Override + public void afterButtonPressed(Object sourceObject) { + Object model = sourceObject != null ? sourceObject : metadata; + if (guiCompositeWidgets != null + && wWidgetsComposite != null + && !wWidgetsComposite.isDisposed()) { + guiCompositeWidgets.setWidgetsContents( + model, wWidgetsComposite, GUI_WIDGETS_PARENT_ID); + wWidgetsComposite.layout(true, true); + } + setChanged(); + } + }); + + setWidgetsContent(); + resetChanged(); + wName.addModifyListener(e -> setChanged()); + } + + @Override + public void setWidgetsContent() { + SparkCatalog meta = this.getMetadata(); + wName.setText(Const.NVL(meta.getName(), "")); + guiCompositeWidgets.setWidgetsContents(metadata, wWidgetsComposite, GUI_WIDGETS_PARENT_ID); + } + + @Override + public void getWidgetsContent(SparkCatalog meta) { + meta.setName(wName.getText()); + guiCompositeWidgets.getWidgetsContents(metadata, GUI_WIDGETS_PARENT_ID); + } + + @Override + public boolean setFocus() { + if (wName == null || wName.isDisposed()) { + return false; + } + return wName.setFocus(); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/metadata/template/SparkCatalogTemplate.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/metadata/template/SparkCatalogTemplate.java new file mode 100644 index 00000000000..adb989395d4 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/metadata/template/SparkCatalogTemplate.java @@ -0,0 +1,258 @@ +/* + * 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.hop.spark.metadata.template; + +import java.util.Arrays; +import java.util.Objects; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.spark.metadata.SparkCatalog; +import org.apache.hop.spark.table.SparkLakeFormats; + +/** + * Named presets for {@link SparkCatalog} fields. Pure data — no SWT. Apply via {@link + * #applyTo(SparkCatalog)}. + * + *

Advanced presets include a commented {@code # docs: …} line in conf extra (ignored by {@code + * SparkCatalogApplier}) so operators can open vendor documentation without leaving Hop. + */ +public enum SparkCatalogTemplate { + ICEBERG_HADOOP_LOCAL( + "Iceberg Hadoop (local)", + "Named Iceberg Hadoop catalog on a local warehouse path. Use TABLE mode as lake.db.table.", + "lake", + SparkCatalog.TYPE_HADOOP, + SparkLakeFormats.ICEBERG_CATALOG, + "file:///tmp/hop-warehouse", + "", + confWithDocs(Docs.ICEBERG_SPARK, "")), + ICEBERG_HADOOP_OBJECT_STORE( + "Iceberg Hadoop (object store)", + "Iceberg Hadoop warehouse on object storage (replace bucket). Credentials via env/Hadoop conf.", + "lake", + SparkCatalog.TYPE_HADOOP, + SparkLakeFormats.ICEBERG_CATALOG, + "s3a://bucket/warehouse", + "", + confWithDocs(Docs.ICEBERG_AWS, "io-impl=org.apache.iceberg.aws.s3.S3FileIO")), + ICEBERG_REST( + "Iceberg REST", + "Iceberg REST catalog. Replace the URI with your catalog service endpoint.", + "lake", + SparkCatalog.TYPE_REST, + SparkLakeFormats.ICEBERG_CATALOG, + "", + "https://catalog.example.com/v1", + confWithDocs(Docs.ICEBERG_REST, "")), + ICEBERG_REST_AUTH( + "Iceberg REST (authenticated)", + "Iceberg REST with token auth. Put the secret in Credential (maps to catalog .token).", + "lake", + SparkCatalog.TYPE_REST, + SparkLakeFormats.ICEBERG_CATALOG, + "", + "https://catalog.example.com/v1", + confWithDocs( + Docs.ICEBERG_REST, + "# Bearer/token: use the Credential field (applied as spark.sql.catalog..token)")), + HIVE_METASTORE( + "Hive Metastore (advanced)", + "Iceberg via Hive Metastore. Requires Hive/Iceberg deps on the cluster (not packaged by default).", + "hive", + SparkCatalog.TYPE_HIVE, + SparkLakeFormats.ICEBERG_CATALOG, + "", + "", + confWithDocs(Docs.ICEBERG_HIVE, "uri=thrift://hive-metastore:9083")), + AWS_GLUE( + "AWS Glue (advanced)", + "Iceberg via AWS Glue. Requires AWS/Iceberg deps and IAM on the cluster (not packaged by default).", + "glue", + SparkCatalog.TYPE_GLUE, + SparkLakeFormats.ICEBERG_CATALOG, + "s3a://bucket/warehouse", + "", + confWithDocs( + Docs.ICEBERG_AWS, + "warehouse=s3a://bucket/warehouse\nio-impl=org.apache.iceberg.aws.s3.S3FileIO")), + NESSIE( + "Nessie (advanced)", + "Iceberg Nessie catalog. Requires Nessie/Iceberg deps; replace URI, ref, and warehouse.", + "nessie", + SparkCatalog.TYPE_CUSTOM, + SparkLakeFormats.ICEBERG_CATALOG, + "file:///tmp/nessie-warehouse", + "http://localhost:19120/api/v1", + confWithDocs( + Docs.NESSIE_SPARK, + "catalog-impl=org.apache.iceberg.nessie.NessieCatalog\n" + + "uri=http://localhost:19120/api/v1\n" + + "ref=main\n" + + "warehouse=file:///tmp/nessie-warehouse")), + DATABRICKS_UNITY( + "Databricks Unity Catalog (advanced)", + "Skeleton for Unity Catalog on Databricks. Prefer workspace-managed conf; adjust URI/auth for your env.", + "unity", + SparkCatalog.TYPE_CUSTOM, + SparkLakeFormats.ICEBERG_CATALOG, + "", + "", + confWithDocs( + Docs.DATABRICKS_UNITY, + "# Typically configured on the Databricks cluster or spark-submit.\n" + + "# Example short keys (uncomment and replace if your runtime expects them):\n" + + "# uri=https:///api/2.1/unity-catalog\n" + + "# warehouse=s3:///unity")), + DELTA_NAMED_CATALOG( + "Delta named catalog (advanced)", + "Named DeltaCatalog (not spark_catalog). Prefer spark_catalog for Delta when coexisting with Iceberg.", + "delta_cat", + SparkCatalog.TYPE_CUSTOM, + SparkLakeFormats.DELTA_CATALOG, + "", + "", + confWithDocs( + Docs.DELTA, + "# Prefer spark.sql.catalog.spark_catalog=DeltaCatalog for default Delta tables.\n" + + "# Use a distinct catalog name only when you must register a second Delta catalog.")); + + /** + * Documentation URLs for conf-extra {@code # docs:} lines. Nested interface so enum constants can + * reference them without illegal forward references. + */ + public interface Docs { + String ICEBERG_SPARK = "https://iceberg.apache.org/docs/latest/spark-configuration/"; + String ICEBERG_REST = "https://iceberg.apache.org/docs/latest/rest-catalog/"; + String ICEBERG_AWS = "https://iceberg.apache.org/docs/latest/aws/"; + String ICEBERG_HIVE = "https://iceberg.apache.org/docs/latest/hive/"; + String NESSIE_SPARK = "https://projectnessie.org/iceberg/spark/"; + String DATABRICKS_UNITY = + "https://docs.databricks.com/en/data-governance/unity-catalog/index.html"; + String DELTA = "https://docs.delta.io/latest/index.html"; + } + + private final String displayName; + private final String description; + private final String catalogName; + private final String catalogType; + private final String implementation; + private final String warehouse; + private final String uri; + private final String confExtra; + + SparkCatalogTemplate( + String displayName, + String description, + String catalogName, + String catalogType, + String implementation, + String warehouse, + String uri, + String confExtra) { + this.displayName = displayName; + this.description = description; + this.catalogName = catalogName; + this.catalogType = catalogType; + this.implementation = implementation; + this.warehouse = warehouse; + this.uri = uri; + this.confExtra = confExtra; + } + + /** + * Build conf-extra text with a leading commented documentation URL (ignored by the applier) and + * optional body lines. + */ + static String confWithDocs(String docsUrl, String body) { + String header = "# docs: " + docsUrl; + if (StringUtils.isBlank(body)) { + return header; + } + return header + "\n" + body; + } + + public String getDisplayName() { + return displayName; + } + + public String getDescription() { + return description; + } + + /** Labels for selection dialogs (same order as {@link #values()}). */ + public static String[] displayNames() { + return Arrays.stream(values()).map(SparkCatalogTemplate::getDisplayName).toArray(String[]::new); + } + + public static SparkCatalogTemplate fromDisplayName(String name) { + if (name == null) { + return null; + } + for (SparkCatalogTemplate t : values()) { + if (t.displayName.equals(name)) { + return t; + } + } + return null; + } + + /** + * Apply this template onto {@code catalog}. Does not change the Hop metadata object name; leaves + * credential empty (never put secrets in presets). + */ + public void applyTo(SparkCatalog catalog) { + Objects.requireNonNull(catalog, "catalog"); + catalog.setCatalogName(catalogName); + catalog.setCatalogType(catalogType); + catalog.setImplementation(implementation); + catalog.setWarehouse(warehouse); + catalog.setUri(uri); + catalog.setCredential(""); + catalog.setConfExtra(confExtra); + } + + /** + * True if the catalog looks customized relative to a fresh {@link SparkCatalog} default (used for + * overwrite confirmation). + */ + public static boolean looksCustomized(SparkCatalog catalog) { + if (catalog == null) { + return false; + } + SparkCatalog def = new SparkCatalog(); + if (StringUtils.isNotEmpty(catalog.getCatalogName())) { + return true; + } + if (!Objects.equals(nz(catalog.getCatalogType()), nz(def.getCatalogType()))) { + return true; + } + if (!Objects.equals(nz(catalog.getImplementation()), nz(def.getImplementation()))) { + return true; + } + if (StringUtils.isNotEmpty(catalog.getWarehouse()) + || StringUtils.isNotEmpty(catalog.getUri()) + || StringUtils.isNotEmpty(catalog.getCredential()) + || StringUtils.isNotEmpty(catalog.getConfExtra())) { + return true; + } + return false; + } + + private static String nz(String s) { + return s == null ? "" : s; + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/HopPipelineMetaToSparkConverter.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/HopPipelineMetaToSparkConverter.java new file mode 100644 index 00000000000..8d16d7e3044 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/HopPipelineMetaToSparkConverter.java @@ -0,0 +1,526 @@ +/* + * 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.hop.spark.pipeline; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.exception.HopRowException; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.logging.LogChannel; +import org.apache.hop.core.metadata.SerializableMetadataProvider; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.PipelineHopMeta; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.config.PipelineRunConfiguration; +import org.apache.hop.pipeline.transform.BaseTransform; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.core.HopSparkUtil; +import org.apache.hop.spark.core.SparkExecutionDataAccumulator; +import org.apache.hop.spark.core.SparkTransformMetricsAccumulator; +import org.apache.hop.spark.engines.ISparkPipelineEngineRunConfiguration; +import org.apache.hop.spark.pipeline.handler.SparkBaseTransformHandler; +import org.apache.hop.spark.pipeline.handler.SparkFileInputHandler; +import org.apache.hop.spark.pipeline.handler.SparkFileOutputHandler; +import org.apache.hop.spark.pipeline.handler.SparkGenericTransformHandler; +import org.apache.hop.spark.pipeline.handler.SparkLakeTableInputHandler; +import org.apache.hop.spark.pipeline.handler.SparkLakeTableMaintenanceHandler; +import org.apache.hop.spark.pipeline.handler.SparkLakeTableMergeHandler; +import org.apache.hop.spark.pipeline.handler.SparkLakeTableOutputHandler; +import org.apache.hop.spark.pipeline.handler.SparkMemoryGroupByHandler; +import org.apache.hop.spark.pipeline.handler.SparkMergeJoinHandler; +import org.apache.hop.spark.pipeline.handler.SparkSortRowsHandler; +import org.apache.hop.spark.pipeline.handler.SparkUniqueRowsHandler; +import org.apache.hop.spark.util.SparkConst; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; + +/** + * Converts a Hop {@link PipelineMeta} into a Spark {@link Dataset} lineage. Stateless transforms + * use mapPartitions; shuffle-aware transforms use native Dataset handlers. + */ +public class HopPipelineMetaToSparkConverter { + + /** + * Plugin ids with dedicated native handlers. Keep in lockstep with {@link + * #addDefaultTransformHandlers()} and {@link + * org.apache.hop.spark.engines.SparkPipelineEngine#supports}. + */ + public static final Set EXPLICIT_HANDLER_PLUGIN_IDS = + Set.of( + SparkConst.MEMORY_GROUP_BY_PLUGIN_ID, + SparkConst.MERGE_JOIN_PLUGIN_ID, + SparkConst.UNIQUE_ROWS_PLUGIN_ID, + SparkConst.SORT_ROWS_PLUGIN_ID, + SparkConst.SPARK_FILE_INPUT_PLUGIN_ID, + SparkConst.SPARK_FILE_OUTPUT_PLUGIN_ID, + SparkConst.SPARK_LAKE_TABLE_INPUT_PLUGIN_ID, + SparkConst.SPARK_LAKE_TABLE_OUTPUT_PLUGIN_ID, + SparkConst.SPARK_LAKE_TABLE_MERGE_PLUGIN_ID, + SparkConst.SPARK_LAKE_TABLE_MAINTENANCE_PLUGIN_ID); + + /** + * Plugin ids that must not run as partition-local Hop mini-pipelines. Keep in lockstep with + * {@link org.apache.hop.spark.engines.SparkPipelineEngine#supports}. + */ + public static final Map HARD_BANNED_PLUGIN_IDS = + Map.of( + SparkConst.GROUP_BY_PLUGIN_ID, + "Group By is not supported on the native Spark engine. Use Memory Group By (native Spark shuffle) instead, or run on Local/Beam."); + + private final IVariables variables; + private final PipelineMeta pipelineMeta; + private final SerializableMetadataProvider metadataProvider; + private final String metaStoreJson; + private final String runConfigName; + private final ISparkPipelineEngineRunConfiguration sparkRunConfiguration; + private final Map transformHandlers; + private final SparkGenericTransformHandler genericTransformHandler; + private SparkTransformMetricsAccumulator metricsAccumulator; + private SparkExecutionDataAccumulator sampleDataAccumulator; + private String parentLogChannelId; + private String dataSamplersJson; + + public HopPipelineMetaToSparkConverter( + IVariables variables, + PipelineMeta pipelineMeta, + IHopMetadataProvider metadataProvider, + String runConfigName) + throws HopException { + this.variables = variables; + this.pipelineMeta = pipelineMeta; + this.metadataProvider = new SerializableMetadataProvider(metadataProvider); + this.metaStoreJson = this.metadataProvider.toJson(); + this.runConfigName = runConfigName; + + PipelineRunConfiguration runConfiguration = + metadataProvider.getSerializer(PipelineRunConfiguration.class).load(runConfigName); + if (runConfiguration == null) { + throw new HopException("Unable to load pipeline run configuration '" + runConfigName + "'"); + } + if (!(runConfiguration.getEngineRunConfiguration() + instanceof ISparkPipelineEngineRunConfiguration sparkConfig)) { + throw new HopException( + "Run configuration '" + + runConfigName + + "' is not a native Spark pipeline engine configuration"); + } + this.sparkRunConfiguration = sparkConfig; + this.transformHandlers = new HashMap<>(); + this.genericTransformHandler = new SparkGenericTransformHandler(); + addDefaultTransformHandlers(); + } + + /** + * Constructor used when the run configuration object is already available (unit tests / engine). + */ + public HopPipelineMetaToSparkConverter( + IVariables variables, + PipelineMeta pipelineMeta, + IHopMetadataProvider metadataProvider, + String runConfigName, + ISparkPipelineEngineRunConfiguration sparkRunConfiguration) + throws HopException { + this.variables = variables; + this.pipelineMeta = pipelineMeta; + this.metadataProvider = new SerializableMetadataProvider(metadataProvider); + this.metaStoreJson = this.metadataProvider.toJson(); + this.runConfigName = runConfigName; + this.sparkRunConfiguration = sparkRunConfiguration; + this.transformHandlers = new HashMap<>(); + this.genericTransformHandler = new SparkGenericTransformHandler(); + addDefaultTransformHandlers(); + } + + protected void addDefaultTransformHandlers() { + transformHandlers.put(SparkConst.MEMORY_GROUP_BY_PLUGIN_ID, new SparkMemoryGroupByHandler()); + transformHandlers.put(SparkConst.MERGE_JOIN_PLUGIN_ID, new SparkMergeJoinHandler()); + transformHandlers.put(SparkConst.UNIQUE_ROWS_PLUGIN_ID, new SparkUniqueRowsHandler()); + transformHandlers.put(SparkConst.SORT_ROWS_PLUGIN_ID, new SparkSortRowsHandler()); + transformHandlers.put(SparkConst.SPARK_FILE_INPUT_PLUGIN_ID, new SparkFileInputHandler()); + transformHandlers.put(SparkConst.SPARK_FILE_OUTPUT_PLUGIN_ID, new SparkFileOutputHandler()); + transformHandlers.put( + SparkConst.SPARK_LAKE_TABLE_INPUT_PLUGIN_ID, new SparkLakeTableInputHandler()); + transformHandlers.put( + SparkConst.SPARK_LAKE_TABLE_OUTPUT_PLUGIN_ID, new SparkLakeTableOutputHandler()); + transformHandlers.put( + SparkConst.SPARK_LAKE_TABLE_MERGE_PLUGIN_ID, new SparkLakeTableMergeHandler()); + transformHandlers.put( + SparkConst.SPARK_LAKE_TABLE_MAINTENANCE_PLUGIN_ID, new SparkLakeTableMaintenanceHandler()); + } + + public void validatePipeline() throws HopException { + for (TransformMeta transformMeta : pipelineMeta.getTransforms()) { + validateTransformSparkUsage(transformMeta.getTransformPluginId(), transformMeta.getName()); + } + } + + public static void validateTransformSparkUsage(String pluginId, String transformName) + throws HopException { + if (pluginId == null) { + return; + } + String reason = HARD_BANNED_PLUGIN_IDS.get(pluginId); + if (reason != null) { + throw new HopException("Transform '" + transformName + "' (" + pluginId + "): " + reason); + } + } + + /** + * Build the Dataset graph and return the sink Dataset (last transform with no successors, or the + * last in topological order). Calling an action on the result materializes the pipeline. + */ + public Dataset createDataset(SparkSession spark) throws HopException { + try { + ILogChannel log = LogChannel.GENERAL; + validatePipeline(); + + Map> transformDatasetMap = new HashMap<>(); + List transforms = getSortedTransformsList(); + + for (TransformMeta transformMeta : transforms) { + String pluginId = transformMeta.getTransformPluginId(); + ISparkPipelineTransformHandler handler = + transformHandlers.getOrDefault(pluginId, genericTransformHandler); + boolean nativeHandler = transformHandlers.containsKey(pluginId); + + List previousTransforms = + pipelineMeta.findPreviousTransforms(transformMeta, false); + + Dataset input = null; + IRowMeta rowMeta; + if (previousTransforms.isEmpty()) { + rowMeta = new org.apache.hop.core.row.RowMeta(); + } else if (nativeHandler && previousTransforms.size() > 1) { + // Merge Join (etc.) resolves named inputs itself from transformDatasetMap + TransformMeta previous = previousTransforms.get(0); + input = lookupPreviousDataset(transformDatasetMap, previous, transformMeta, log); + rowMeta = + input != null + ? pipelineMeta.getTransformFields(variables, previous, transformMeta, null) + : new org.apache.hop.core.row.RowMeta(); + } else { + // One or more main predecessors: resolve each Dataset (incl. target-stream keys) and + // union when layouts match (Hop multi-hop merge semantics). + ResolvedInputs resolved = + resolveAndUnionInputs( + log, + variables, + pipelineMeta, + transformMeta, + previousTransforms, + transformDatasetMap); + input = resolved.dataset(); + rowMeta = resolved.rowMeta(); + if (input == null) { + throw new HopException( + "Previous Dataset(s) for transform '" + + transformMeta.getName() + + "' could not be found (previous=" + + previousTransforms.stream().map(TransformMeta::getName).toList() + + "). Check that hops into this transform are enabled."); + } + } + + handler.handleTransform( + log, + variables, + runConfigName, + sparkRunConfiguration, + metadataProvider, + metaStoreJson, + pipelineMeta, + transformMeta, + transformDatasetMap, + spark, + rowMeta, + previousTransforms, + input); + } + + // Prefer a leaf transform as the result + for (int i = transforms.size() - 1; i >= 0; i--) { + TransformMeta candidate = transforms.get(i); + if (pipelineMeta.findNextTransforms(candidate).isEmpty()) { + Dataset leaf = transformDatasetMap.get(candidate.getName()); + if (leaf != null) { + return leaf; + } + } + } + if (transforms.isEmpty()) { + throw new HopException("Pipeline has no transforms to execute on Spark"); + } + Dataset last = transformDatasetMap.get(transforms.get(transforms.size() - 1).getName()); + if (last == null) { + throw new HopException("No Spark Dataset was produced for the pipeline"); + } + return last; + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException("Error converting Hop pipeline to Spark", e); + } + } + + /** + * Resolve Datasets for all main previous transforms (preferring target-stream keys), verify that + * Hop row layouts match, and {@code union} them. Matches Beam Flatten / Hop multi-hop merge. + */ + static ResolvedInputs resolveAndUnionInputs( + ILogChannel log, + IVariables variables, + PipelineMeta pipelineMeta, + TransformMeta transformMeta, + List previousTransforms, + Map> transformDatasetMap) + throws HopException { + if (previousTransforms == null || previousTransforms.isEmpty()) { + return new ResolvedInputs(null, new org.apache.hop.core.row.RowMeta()); + } + + List> datasets = new ArrayList<>(); + IRowMeta referenceRowMeta = null; + List sourceNames = new ArrayList<>(); + + for (TransformMeta previous : previousTransforms) { + Dataset ds = lookupPreviousDataset(transformDatasetMap, previous, transformMeta, log); + if (ds == null) { + throw new HopException( + "Previous Dataset for transform '" + + previous.getName() + + "' could not be found when handling '" + + transformMeta.getName() + + "'"); + } + // Pass current as targetTransform so hops like Workflow Executor → results keep + // execution-result fields (getFields clears the row when nextTransform is null). + IRowMeta prevRowMeta = + pipelineMeta.getTransformFields(variables, previous, transformMeta, null); + if (referenceRowMeta == null) { + referenceRowMeta = prevRowMeta; + } else if (!transformMeta.getTransform().excludeFromRowLayoutVerification()) { + try { + BaseTransform.safeModeChecking(referenceRowMeta, prevRowMeta); + } catch (HopRowException e) { + throw new HopException( + "Cannot combine data into transform '" + + transformMeta.getName() + + "': previous transforms do not share the same row layout. Hop requires" + + " identical field names, order, and types when multiple hops feed one" + + " transform. Mismatch involving '" + + previous.getName() + + "': " + + e.getMessage(), + e); + } + } + datasets.add(ds); + sourceNames.add(previous.getName()); + } + + Dataset unioned = datasets.get(0); + for (int i = 1; i < datasets.size(); i++) { + // unionByName is more resilient if Spark column order drifts; layouts already checked + unioned = unioned.unionByName(datasets.get(i)); + } + if (datasets.size() > 1) { + log.logBasic( + "Combined " + + datasets.size() + + " previous Dataset(s) into transform '" + + transformMeta.getName() + + "': " + + sourceNames); + } + return new ResolvedInputs( + unioned, + referenceRowMeta != null ? referenceRowMeta : new org.apache.hop.core.row.RowMeta()); + } + + /** + * Prefer a target-stream Dataset when {@code previous} routed to {@code current} (Filter/Switch); + * otherwise use the previous transform's main Dataset. + */ + static Dataset lookupPreviousDataset( + Map> transformDatasetMap, + TransformMeta previous, + TransformMeta current, + ILogChannel log) { + String targetKey = HopSparkUtil.createTargetTupleId(previous.getName(), current.getName()); + Dataset input = transformDatasetMap.get(targetKey); + if (input != null) { + if (log != null) { + log.logBasic( + "Transform '" + + current.getName() + + "' reading from previous target stream key: " + + targetKey); + } + return input; + } + return transformDatasetMap.get(previous.getName()); + } + + /** Result of resolving one or more previous Datasets for a transform. */ + record ResolvedInputs(Dataset dataset, IRowMeta rowMeta) {} + + /** + * Transforms that participate in at least one enabled hop (Beam-style active + * graph). Fully disconnected transforms and transforms only linked by disabled hops are skipped + * so sinks without an active input are not executed. + * + *

Unlike {@link PipelineMeta#getPipelineHopTransforms(boolean)}, this does not re-add + * unused canvas transforms (those exist for painting only). + */ + public static List collectActiveTransforms(PipelineMeta pipelineMeta) { + if (pipelineMeta == null) { + return List.of(); + } + Set active = new LinkedHashSet<>(); + List hops = pipelineMeta.getPipelineHops(); + if (hops != null) { + for (PipelineHopMeta hop : hops) { + if (hop == null || !hop.isEnabled()) { + continue; + } + if (hop.getFromTransform() != null) { + active.add(hop.getFromTransform()); + } + if (hop.getToTransform() != null) { + active.add(hop.getToTransform()); + } + } + } + // Single-transform pipeline with no hops (rare design canvas) + if (active.isEmpty() && pipelineMeta.nrTransforms() == 1) { + active.add(pipelineMeta.getTransform(0)); + } + return new ArrayList<>(active); + } + + /** Topological order of active transforms (Kahn-style via repeated previous-set checks). */ + protected List getSortedTransformsList() throws HopException { + List transforms = collectActiveTransforms(pipelineMeta); + if (transforms.isEmpty() && pipelineMeta.nrTransforms() > 0) { + throw new HopException( + "No active transforms to execute on Spark: enable at least one hop between transforms" + + " (disabled hops and disconnected transforms are skipped)."); + } + List sorted = new ArrayList<>(); + Set handled = new HashSet<>(); + Set activeSet = new HashSet<>(transforms); + + while (sorted.size() < transforms.size()) { + boolean progress = false; + for (TransformMeta transformMeta : transforms) { + if (handled.contains(transformMeta)) { + continue; + } + // Include informational predecessors so Stream Lookup sources are built first + List previous = pipelineMeta.findPreviousTransforms(transformMeta, true); + boolean allPreviousHandled = true; + for (TransformMeta prev : previous) { + // Only require predecessors that are themselves active in the enabled graph + if (activeSet.contains(prev) && !handled.contains(prev)) { + allPreviousHandled = false; + break; + } + } + if (allPreviousHandled) { + sorted.add(transformMeta); + handled.add(transformMeta); + progress = true; + } + } + if (!progress) { + // Cycle or unresolved graph — fall back to original order for remaining + for (TransformMeta transformMeta : transforms) { + if (!handled.contains(transformMeta)) { + sorted.add(transformMeta); + handled.add(transformMeta); + } + } + } + } + return sorted; + } + + public ISparkPipelineEngineRunConfiguration getSparkRunConfiguration() { + return sparkRunConfiguration; + } + + /** + * Registers the driver-side metrics accumulator used by generic mapPartitions transforms and + * native Dataset handlers. Must be called after the accumulator is registered with the Spark + * context. + */ + public void setMetricsAccumulator(SparkTransformMetricsAccumulator metricsAccumulator) { + this.metricsAccumulator = metricsAccumulator; + this.genericTransformHandler.setMetricsAccumulator(metricsAccumulator); + for (ISparkPipelineTransformHandler handler : transformHandlers.values()) { + if (handler instanceof SparkBaseTransformHandler baseHandler) { + baseHandler.setMetricsAccumulator(metricsAccumulator); + } + } + } + + public SparkTransformMetricsAccumulator getMetricsAccumulator() { + return metricsAccumulator; + } + + /** + * Accumulator that carries sample {@code ExecutionData} JSON from executors to the driver for + * registration at the driver's execution info location. + */ + public void setSampleDataAccumulator(SparkExecutionDataAccumulator sampleDataAccumulator) { + this.sampleDataAccumulator = sampleDataAccumulator; + this.genericTransformHandler.setSampleDataAccumulator(sampleDataAccumulator); + } + + public SparkExecutionDataAccumulator getSampleDataAccumulator() { + return sampleDataAccumulator; + } + + /** + * Context for executor-side execution data sampling. Call after the engine log channel exists. + * Samples are shipped to the driver via {@link #setSampleDataAccumulator}. + */ + public void setExecutionSamplingContext(String parentLogChannelId, String dataSamplersJson) { + this.parentLogChannelId = parentLogChannelId; + this.dataSamplersJson = dataSamplersJson; + this.genericTransformHandler.setExecutionSamplingContext( + runConfigName, parentLogChannelId, dataSamplersJson); + } + + public String getParentLogChannelId() { + return parentLogChannelId; + } + + public String getDataSamplersJson() { + return dataSamplersJson; + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/ISparkPipelineTransformHandler.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/ISparkPipelineTransformHandler.java new file mode 100644 index 00000000000..83d493d55f7 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/ISparkPipelineTransformHandler.java @@ -0,0 +1,55 @@ +/* + * 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.hop.spark.pipeline; + +import java.util.List; +import java.util.Map; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.engines.ISparkPipelineEngineRunConfiguration; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; + +public interface ISparkPipelineTransformHandler { + + boolean isInput(); + + boolean isOutput(); + + void handleTransform( + ILogChannel log, + IVariables variables, + String runConfigurationName, + ISparkPipelineEngineRunConfiguration runConfiguration, + IHopMetadataProvider metadataProvider, + String metastoreJson, + PipelineMeta pipelineMeta, + TransformMeta transformMeta, + Map> transformDatasetMap, + SparkSession spark, + IRowMeta rowMeta, + List previousTransforms, + Dataset input) + throws HopException; +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkBaseTransformHandler.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkBaseTransformHandler.java new file mode 100644 index 00000000000..2ee63a591ec --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkBaseTransformHandler.java @@ -0,0 +1,88 @@ +/* + * 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.hop.spark.pipeline.handler; + +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.xml.XmlHandler; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.ITransformMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.core.SparkNativeMetrics; +import org.apache.hop.spark.core.SparkTransformMetricsAccumulator; +import org.apache.hop.spark.pipeline.ISparkPipelineTransformHandler; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.w3c.dom.Node; + +public abstract class SparkBaseTransformHandler implements ISparkPipelineTransformHandler { + + private SparkTransformMetricsAccumulator metricsAccumulator; + + public void setMetricsAccumulator(SparkTransformMetricsAccumulator metricsAccumulator) { + this.metricsAccumulator = metricsAccumulator; + } + + protected SparkTransformMetricsAccumulator getMetricsAccumulator() { + return metricsAccumulator; + } + + /** + * Attach native row tracking for this transform when a metrics accumulator is registered. Returns + * {@code dataset} unchanged when metrics are disabled. + */ + protected Dataset trackMetrics( + Dataset dataset, TransformMeta transformMeta, SparkNativeMetrics.Role role) { + if (dataset == null || transformMeta == null) { + return dataset; + } + return SparkNativeMetrics.track(dataset, transformMeta.getName(), metricsAccumulator, role); + } + + @Override + public boolean isInput() { + return false; + } + + @Override + public boolean isOutput() { + return false; + } + + protected Node getTransformXmlNode(TransformMeta transformMeta) throws HopException { + String xml = transformMeta.getXml(); + return XmlHandler.getSubNode(XmlHandler.loadXmlString(xml), TransformMeta.XML_TAG); + } + + protected void loadTransformMetadata( + ITransformMeta meta, + TransformMeta transformMeta, + IHopMetadataProvider metadataProvider, + PipelineMeta pipelineMeta) + throws HopException { + // Prefer live instance when classloaders match (normal Hop driver) + if (meta.getClass().isInstance(transformMeta.getTransform())) { + ITransformMeta live = transformMeta.getTransform(); + // Copy via XML round-trip so we never mutate the live pipeline meta + meta.loadXml(getTransformXmlNode(transformMeta), metadataProvider); + } else { + meta.loadXml(getTransformXmlNode(transformMeta), metadataProvider); + } + meta.searchInfoAndTargetTransforms(pipelineMeta.getTransforms()); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkFileInputHandler.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkFileInputHandler.java new file mode 100644 index 00000000000..851e57c27bd --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkFileInputHandler.java @@ -0,0 +1,380 @@ +/* + * 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.hop.spark.pipeline.handler; + +import static org.apache.spark.sql.functions.col; +import static org.apache.spark.sql.functions.lit; +import static org.apache.spark.sql.functions.to_date; +import static org.apache.spark.sql.functions.to_timestamp; +import static org.apache.spark.sql.functions.trim; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.row.IValueMeta; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.core.SparkNativeMetrics; +import org.apache.hop.spark.engines.ISparkPipelineEngineRunConfiguration; +import org.apache.hop.spark.pkg.SparkProjectPackage; +import org.apache.hop.spark.transforms.io.SparkField; +import org.apache.hop.spark.transforms.io.SparkFileInputMeta; +import org.apache.hop.spark.util.SparkPathDialect; +import org.apache.spark.sql.Column; +import org.apache.spark.sql.DataFrameReader; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.types.DataTypes; + +/** + * Native Spark file read for {@link SparkFileInputMeta}. + * + *

CSV/text with an explicit field list is read as strings first, then projected/cast by + * column name. Applying a StructType schema directly to the CSV reader maps by position, + * so omitting a middle column (or a header/schema mismatch) silently shifts values under the wrong + * names. + */ +public class SparkFileInputHandler extends SparkBaseTransformHandler { + + /** Common Hop/Spark date masks used when a field has no format set. */ + private static final String[] DEFAULT_DATE_FORMATS = { + "yyyy/MM/dd", "yyyy-MM-dd", "MM/dd/yyyy", "dd/MM/yyyy", "yyyyMMdd" + }; + + private static final String[] DEFAULT_TIMESTAMP_FORMATS = { + "yyyy/MM/dd HH:mm:ss", + "yyyy-MM-dd HH:mm:ss", + "yyyy/MM/dd'T'HH:mm:ss", + "yyyy-MM-dd'T'HH:mm:ss", + "yyyy/MM/dd", + "yyyy-MM-dd" + }; + + @Override + public boolean isInput() { + return true; + } + + @Override + public void handleTransform( + ILogChannel log, + IVariables variables, + String runConfigurationName, + ISparkPipelineEngineRunConfiguration runConfiguration, + IHopMetadataProvider metadataProvider, + String metastoreJson, + PipelineMeta pipelineMeta, + TransformMeta transformMeta, + Map> transformDatasetMap, + SparkSession spark, + IRowMeta rowMeta, + List previousTransforms, + Dataset input) + throws HopException { + + SparkFileInputMeta meta = new SparkFileInputMeta(); + loadTransformMetadata(meta, transformMeta, metadataProvider, pipelineMeta); + + if (log != null) { + String phWarn = SparkProjectPackage.projectHomeDataPathWarning(meta.getFilePath()); + if (phWarn != null) { + log.logBasic("WARNING: " + phWarn); + } + } + String resolved = variables.resolve(meta.getFilePath()); + String path = SparkPathDialect.toSparkUri(resolved, runConfiguration); + if (StringUtils.isEmpty(path)) { + throw new HopException( + "Spark File Input '" + transformMeta.getName() + "' has no file path configured"); + } + if (log != null && StringUtils.isNotEmpty(resolved) && !resolved.trim().equals(path)) { + log.logBasic( + "Spark File Input '" + + transformMeta.getName() + + "' path scheme map: '" + + resolved + + "' -> '" + + path + + "'"); + } + + String format = SparkFileIoSupport.normalizeFormat(meta.getFileFormat()); + DataFrameReader reader = spark.read().format(format); + + Map options = + SparkFileIoSupport.parseExtraOptions(variables, meta.getExtraOptions()); + boolean delimited = + SparkFileInputMeta.FORMAT_CSV.equals(format) + || SparkFileInputMeta.FORMAT_TEXT.equals(format); + + if (delimited) { + options.putIfAbsent("header", Boolean.toString(meta.isHeader())); + if (StringUtils.isNotEmpty(meta.getSeparator())) { + options.putIfAbsent("sep", variables.resolve(meta.getSeparator())); + } + if (StringUtils.isNotEmpty(meta.getQuote())) { + options.putIfAbsent("quote", variables.resolve(meta.getQuote())); + } + if (meta.isMultiLine()) { + options.putIfAbsent("multiLine", "true"); + } + // Leading spaces in numeric fields (" 1", " 13520") are common in Hop samples + options.putIfAbsent("ignoreLeadingWhiteSpace", "true"); + options.putIfAbsent("ignoreTrailingWhiteSpace", "true"); + // Prefer permissive mode so a bad date/int does not fail the whole job + options.putIfAbsent("mode", "PERMISSIVE"); + } + + for (Map.Entry e : options.entrySet()) { + reader = reader.option(e.getKey(), e.getValue()); + } + + boolean hasFields = meta.getFields() != null && !meta.getFields().isEmpty(); + boolean nameBasedProjection = delimited && hasFields && !meta.isInferSchema(); + + // For delimited + explicit fields: do NOT push StructType into the reader (positional). + // Infer schema only when requested and no field list. + if (delimited && meta.isInferSchema() && !hasFields) { + reader = reader.option("inferSchema", "true"); + } + + Dataset dataset; + try { + dataset = reader.load(path); + } catch (Exception e) { + throw new HopException( + SparkPathDialect.withPathHint( + "Error reading '" + + path + + "' as " + + format + + " in transform '" + + transformMeta.getName() + + "'", + path), + e); + } + + if (nameBasedProjection) { + // header=false → Spark names columns _c0,_c1,... — map by position to the field list + if (!meta.isHeader() && isPositionalSparkCsvColumns(dataset.columns())) { + dataset = projectAndCastByPosition(log, transformMeta.getName(), dataset, meta.getFields()); + } else { + dataset = projectAndCastByName(log, transformMeta.getName(), dataset, meta.getFields()); + } + } else if (hasFields && !delimited) { + // Parquet/ORC/JSON: select by name + cast when a field list is provided + dataset = projectAndCastByName(log, transformMeta.getName(), dataset, meta.getFields()); + } + + dataset = trackMetrics(dataset, transformMeta, SparkNativeMetrics.Role.INPUT); + transformDatasetMap.put(transformMeta.getName(), dataset); + log.logBasic( + "Handled Spark File Input : " + + transformMeta.getName() + + " format=" + + format + + " path=" + + path + + " columns=" + + Arrays.toString(dataset.columns())); + } + + /** True when Spark assigned default positional CSV names (_c0, _c1, …). */ + static boolean isPositionalSparkCsvColumns(String[] columns) { + if (columns == null || columns.length == 0) { + return false; + } + for (int i = 0; i < columns.length; i++) { + if (!("_c" + i).equals(columns[i])) { + return false; + } + } + return true; + } + + /** + * Map Spark's default {@code _cN} columns to the configured field list by position (no-header + * CSV). Extra file columns are dropped; missing positions become null. + */ + static Dataset projectAndCastByPosition( + ILogChannel log, String transformName, Dataset source, List fields) + throws HopException { + String[] columns = source.columns(); + List projected = new ArrayList<>(); + int i = 0; + for (SparkField field : fields) { + if (StringUtils.isEmpty(field.getName())) { + continue; + } + if (i < columns.length) { + projected.add(castColumn(source, columns[i], field).alias(field.getName())); + } else { + if (log != null) { + log.logError( + "Spark File Input '" + + transformName + + "': no file column at position " + + i + + " for field '" + + field.getName() + + "' — filled with nulls"); + } + projected.add(lit(null).cast(DataTypes.StringType).alias(field.getName())); + } + i++; + } + if (projected.isEmpty()) { + throw new HopException( + "Spark File Input '" + transformName + "' field list produced no output columns"); + } + if (log != null) { + log.logBasic( + "Spark File Input '" + + transformName + + "': mapped " + + Math.min(i, columns.length) + + " positional column(s) (_cN) to field list (header=false)"); + } + return source.select(projected.toArray(new Column[0])); + } + + /** + * Select and cast columns by name in the order of {@code fields}. Extra file columns are dropped; + * missing names become null columns with a warning. + */ + public static Dataset projectAndCastByName( + ILogChannel log, String transformName, Dataset source, List fields) + throws HopException { + + Set available = + Arrays.stream(source.columns()).collect(Collectors.toCollection(HashSet::new)); + // Case-insensitive lookup map for header names + Map availableByLower = new java.util.LinkedHashMap<>(); + for (String c : source.columns()) { + availableByLower.put(c.toLowerCase(Locale.ROOT), c); + } + + List missing = new ArrayList<>(); + List unused = new ArrayList<>(available); + List projected = new ArrayList<>(); + + for (SparkField field : fields) { + if (StringUtils.isEmpty(field.getName())) { + continue; + } + String want = field.getName(); + String actual = + available.contains(want) ? want : availableByLower.get(want.toLowerCase(Locale.ROOT)); + if (actual == null) { + missing.add(want); + projected.add(lit(null).cast(DataTypes.StringType).alias(want)); + continue; + } + unused.remove(actual); + projected.add(castColumn(source, actual, field).alias(want)); + } + + if (!missing.isEmpty() && log != null) { + log.logError( + "Spark File Input '" + + transformName + + "': field(s) not found in file header " + + Arrays.toString(source.columns()) + + ": " + + missing + + " — filled with nulls"); + } + if (!unused.isEmpty() && log != null) { + log.logBasic( + "Spark File Input '" + + transformName + + "': file column(s) not in field list (dropped): " + + unused); + } + if (projected.isEmpty()) { + throw new HopException( + "Spark File Input '" + transformName + "' field list produced no output columns"); + } + + return source.select(projected.toArray(new Column[0])); + } + + private static Column castColumn(Dataset source, String columnName, SparkField field) + throws HopException { + Column c = col(columnName); + // Always trim strings before numeric/date conversion + Column trimmed = trim(c.cast(DataTypes.StringType)); + + String hopType = StringUtils.defaultIfBlank(field.getHopType(), "String"); + int typeId; + try { + typeId = org.apache.hop.core.row.value.ValueMetaFactory.getIdForValueMeta(hopType); + } catch (Exception e) { + throw new HopException( + "Unknown Hop type '" + hopType + "' for field '" + field.getName() + "'", e); + } + + return switch (typeId) { + case IValueMeta.TYPE_STRING, IValueMeta.TYPE_INET -> trimmed.alias(field.getName()); + case IValueMeta.TYPE_INTEGER -> trimmed.cast(DataTypes.LongType); + case IValueMeta.TYPE_NUMBER -> trimmed.cast(DataTypes.DoubleType); + case IValueMeta.TYPE_BIGNUMBER -> trimmed.cast(DataTypes.createDecimalType(38, 18)); + case IValueMeta.TYPE_BOOLEAN -> trimmed.cast(DataTypes.BooleanType); + case IValueMeta.TYPE_DATE -> parseDate(trimmed, field.getFormatMask(), false); + case IValueMeta.TYPE_TIMESTAMP -> parseDate(trimmed, field.getFormatMask(), true); + case IValueMeta.TYPE_BINARY -> c.cast(DataTypes.BinaryType); + default -> trimmed; + }; + } + + /** + * Parse date/timestamp strings. Uses the field format mask when set; otherwise tries common Hop + * masks (including {@code yyyy/MM/dd}). + */ + private static Column parseDate(Column stringCol, String formatMask, boolean timestamp) { + if (StringUtils.isNotEmpty(formatMask)) { + return timestamp + ? to_timestamp(stringCol, formatMask) + : to_date(stringCol, formatMask).cast(DataTypes.TimestampType); + } + String[] formats = timestamp ? DEFAULT_TIMESTAMP_FORMATS : DEFAULT_DATE_FORMATS; + // coalesce(to_timestamp(c, f1), to_timestamp(c, f2), ...) + Column parsed = null; + for (String f : formats) { + Column attempt = + timestamp + ? to_timestamp(stringCol, f) + : to_date(stringCol, f).cast(DataTypes.TimestampType); + parsed = parsed == null ? attempt : org.apache.spark.sql.functions.coalesce(parsed, attempt); + } + return parsed; + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkFileIoSupport.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkFileIoSupport.java new file mode 100644 index 00000000000..3c3c2f1239b --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkFileIoSupport.java @@ -0,0 +1,106 @@ +/* + * 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.hop.spark.pipeline.handler; + +import java.util.LinkedHashMap; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.spark.transforms.io.SparkFileInputMeta; +import org.apache.hop.spark.util.SparkPathDialect; +import org.apache.spark.sql.DataFrameWriter; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SaveMode; + +/** Shared helpers for Spark file source/sink handlers. */ +public final class SparkFileIoSupport { + + private SparkFileIoSupport() {} + + public static String normalizeFormat(String format) { + if (StringUtils.isEmpty(format)) { + return SparkFileInputMeta.FORMAT_CSV; + } + return format.trim().toLowerCase(); + } + + public static Map parseExtraOptions(IVariables variables, String extraOptions) { + Map options = new LinkedHashMap<>(); + if (StringUtils.isEmpty(extraOptions)) { + return options; + } + for (String line : extraOptions.split("\\r?\\n")) { + String trimmed = line.trim(); + if (trimmed.isEmpty() || trimmed.startsWith("#")) { + continue; + } + int eq = trimmed.indexOf('='); + if (eq > 0) { + String key = variables.resolve(trimmed.substring(0, eq).trim()); + String value = variables.resolve(trimmed.substring(eq + 1).trim()); + options.put(key, value); + } + } + return options; + } + + public static SaveMode toSaveMode(String mode) throws HopException { + if (StringUtils.isEmpty(mode)) { + return SaveMode.Overwrite; + } + return switch (mode.trim()) { + case "Overwrite", "overwrite" -> SaveMode.Overwrite; + case "Append", "append" -> SaveMode.Append; + case "Ignore", "ignore" -> SaveMode.Ignore; + case "ErrorIfExists", "error", "Error" -> SaveMode.ErrorIfExists; + default -> throw new HopException("Unknown Spark save mode: " + mode); + }; + } + + public static void writeDataset( + Dataset dataset, + String format, + String path, + SaveMode saveMode, + Map options, + String[] partitionColumns, + Integer coalesce) + throws HopException { + Dataset toWrite = dataset; + if (coalesce != null && coalesce > 0) { + toWrite = toWrite.coalesce(coalesce); + } + DataFrameWriter writer = toWrite.write().mode(saveMode).format(format); + for (Map.Entry e : options.entrySet()) { + writer = writer.option(e.getKey(), e.getValue()); + } + if (partitionColumns != null && partitionColumns.length > 0) { + writer = writer.partitionBy(partitionColumns); + } + try { + writer.save(path); + } catch (Exception e) { + throw new HopException( + SparkPathDialect.withPathHint( + "Error writing Spark Dataset to '" + path + "' as " + format, path), + e); + } + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkFileOutputHandler.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkFileOutputHandler.java new file mode 100644 index 00000000000..72388cb338f --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkFileOutputHandler.java @@ -0,0 +1,166 @@ +/* + * 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.hop.spark.pipeline.handler; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.Const; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.core.SparkNativeMetrics; +import org.apache.hop.spark.engines.ISparkPipelineEngineRunConfiguration; +import org.apache.hop.spark.pkg.SparkProjectPackage; +import org.apache.hop.spark.transforms.io.SparkFileInputMeta; +import org.apache.hop.spark.transforms.io.SparkFileOutputMeta; +import org.apache.hop.spark.util.SparkPathDialect; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SaveMode; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.types.DataTypes; +import org.apache.spark.sql.types.StructType; + +/** + * Native Spark file write for {@link SparkFileOutputMeta}. Executes the write as an action and + * registers an empty leaf Dataset so a later engine-level {@code count()} does not re-run the + * write. + */ +public class SparkFileOutputHandler extends SparkBaseTransformHandler { + + @Override + public boolean isOutput() { + return true; + } + + @Override + public void handleTransform( + ILogChannel log, + IVariables variables, + String runConfigurationName, + ISparkPipelineEngineRunConfiguration runConfiguration, + IHopMetadataProvider metadataProvider, + String metastoreJson, + PipelineMeta pipelineMeta, + TransformMeta transformMeta, + Map> transformDatasetMap, + SparkSession spark, + IRowMeta rowMeta, + List previousTransforms, + Dataset input) + throws HopException { + + if (input == null) { + throw new HopException( + "Spark File Output '" + + transformMeta.getName() + + "' has no input Dataset. Connect an enabled hop from an upstream transform" + + " (disabled hops and disconnected transforms are not executed on native Spark)."); + } + + SparkFileOutputMeta meta = new SparkFileOutputMeta(); + loadTransformMetadata(meta, transformMeta, metadataProvider, pipelineMeta); + + if (log != null) { + String phWarn = SparkProjectPackage.projectHomeDataPathWarning(meta.getFilePath()); + if (phWarn != null) { + log.logBasic("WARNING: " + phWarn); + } + } + String resolved = variables.resolve(meta.getFilePath()); + String path = SparkPathDialect.toSparkUri(resolved, runConfiguration); + if (StringUtils.isEmpty(path)) { + throw new HopException( + "Spark File Output '" + transformMeta.getName() + "' has no file path configured"); + } + if (log != null && StringUtils.isNotEmpty(resolved) && !resolved.trim().equals(path)) { + log.logBasic( + "Spark File Output '" + + transformMeta.getName() + + "' path scheme map: '" + + resolved + + "' -> '" + + path + + "'"); + } + + String format = SparkFileIoSupport.normalizeFormat(meta.getFileFormat()); + SaveMode saveMode = SparkFileIoSupport.toSaveMode(meta.getSaveMode()); + + Map options = + SparkFileIoSupport.parseExtraOptions(variables, meta.getExtraOptions()); + if (SparkFileInputMeta.FORMAT_CSV.equals(format) + || SparkFileInputMeta.FORMAT_TEXT.equals(format)) { + options.putIfAbsent("header", Boolean.toString(meta.isHeader())); + if (StringUtils.isNotEmpty(meta.getSeparator())) { + options.putIfAbsent("sep", variables.resolve(meta.getSeparator())); + } + if (StringUtils.isNotEmpty(meta.getQuote())) { + options.putIfAbsent("quote", variables.resolve(meta.getQuote())); + } + } + + String[] partitionColumns = null; + if (StringUtils.isNotEmpty(meta.getPartitionByColumns())) { + List parts = new ArrayList<>(); + for (String p : meta.getPartitionByColumns().split(",")) { + if (StringUtils.isNotEmpty(p.trim())) { + parts.add(p.trim()); + } + } + if (!parts.isEmpty()) { + partitionColumns = parts.toArray(new String[0]); + } + } + + Integer coalesce = null; + if (StringUtils.isNotEmpty(meta.getCoalescePartitions())) { + coalesce = Const.toInt(variables.resolve(meta.getCoalescePartitions()), -1); + if (coalesce <= 0) { + coalesce = null; + } + } + + // Count rows as they flow into the write (action materializes this lineage) + Dataset toWrite = trackMetrics(input, transformMeta, SparkNativeMetrics.Role.OUTPUT); + SparkFileIoSupport.writeDataset( + toWrite, format, path, saveMode, options, partitionColumns, coalesce); + + // Leaf marker: write already ran; avoid re-triggering via a later count() + StructType emptySchema = + new StructType().add("_spark_hop_written", DataTypes.BooleanType, false); + Dataset leaf = spark.createDataFrame(new ArrayList<>(), emptySchema); + transformDatasetMap.put(transformMeta.getName(), leaf); + + log.logBasic( + "Handled Spark File Output : " + + transformMeta.getName() + + " format=" + + format + + " mode=" + + saveMode + + " path=" + + path); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkGenericTransformHandler.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkGenericTransformHandler.java new file mode 100644 index 00000000000..27853707ba0 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkGenericTransformHandler.java @@ -0,0 +1,457 @@ +/* + * 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.hop.spark.pipeline.handler; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.row.JsonRowMeta; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.core.xml.XmlHandler; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.ITransformIOMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.pipeline.transform.stream.IStream; +import org.apache.hop.spark.core.HopMapPartitionsFn; +import org.apache.hop.spark.core.HopSparkRowConverter; +import org.apache.hop.spark.core.HopSparkUtil; +import org.apache.hop.spark.core.SparkExecutionDataAccumulator; +import org.apache.hop.spark.core.SparkInfoStreamSupport; +import org.apache.hop.spark.core.SparkTransformMetricsAccumulator; +import org.apache.hop.spark.core.SparkVariableValue; +import org.apache.hop.spark.engines.ISparkPipelineEngineRunConfiguration; +import org.apache.hop.spark.pipeline.ISparkPipelineTransformHandler; +import org.apache.hop.spark.util.SparkRunMode; +import org.apache.spark.api.java.JavaRDD; +import org.apache.spark.broadcast.Broadcast; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.RowFactory; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.types.StructType; + +/** + * Wraps a Hop transform in {@link HopMapPartitionsFn} so initialization happens once per Spark + * partition (DISTRIBUTED) or once on the Spark driver (DRIVER_ONLY). Supports optional info/side + * streams (Stream Lookup, etc.) via broadcast of collected rows. + */ +public class SparkGenericTransformHandler implements ISparkPipelineTransformHandler { + + /** Soft warning threshold when materializing input rows on the driver. */ + static final long DRIVER_ONLY_ROW_WARN_THRESHOLD = 100_000L; + + private SparkTransformMetricsAccumulator metricsAccumulator; + private SparkExecutionDataAccumulator sampleDataAccumulator; + private String runConfigName; + private String parentLogChannelId; + private String dataSamplersJson; + + public void setMetricsAccumulator(SparkTransformMetricsAccumulator metricsAccumulator) { + this.metricsAccumulator = metricsAccumulator; + } + + public void setSampleDataAccumulator(SparkExecutionDataAccumulator sampleDataAccumulator) { + this.sampleDataAccumulator = sampleDataAccumulator; + } + + public void setExecutionSamplingContext( + String runConfigName, String parentLogChannelId, String dataSamplersJson) { + this.runConfigName = runConfigName; + this.parentLogChannelId = parentLogChannelId; + this.dataSamplersJson = dataSamplersJson; + } + + @Override + public boolean isInput() { + return false; + } + + @Override + public boolean isOutput() { + return false; + } + + @Override + public void handleTransform( + ILogChannel log, + IVariables variables, + String runConfigurationName, + ISparkPipelineEngineRunConfiguration runConfiguration, + IHopMetadataProvider metadataProvider, + String metastoreJson, + PipelineMeta pipelineMeta, + TransformMeta transformMeta, + Map> transformDatasetMap, + SparkSession spark, + IRowMeta rowMeta, + List previousTransforms, + Dataset input) + throws HopException { + + boolean inputTransform = input == null; + // previousTransforms are main predecessors only (converter uses findPreviousTransforms(..., + // false)). Multiple main predecessors are already unioned by HopPipelineMetaToSparkConverter + // when row layouts match. + + // Ensure target stream subjects are bound (pipeline load usually does this already) + transformMeta.getTransform().searchInfoAndTargetTransforms(pipelineMeta.getTransforms()); + List targetNames = resolveTargetTransformNames(transformMeta); + + List nextTransforms = pipelineMeta.findNextTransforms(transformMeta); + if (nextTransforms.size() > 1 && targetNames.isEmpty()) { + throw new HopException( + "Multiple outgoing hops from transform '" + + transformMeta.getName() + + "' are not supported on the native Spark engine unless they are declared as" + + " target streams (e.g. Filter Rows true/false, Switch/Case). plugin id=" + + transformMeta.getTransformPluginId()); + } + + // Info/side streams: all previous including informational minus main previous + List infoTransformMetas = + resolveInfoTransforms(pipelineMeta, transformMeta, previousTransforms); + + List infoNames = new ArrayList<>(); + List infoRowMetaJsons = new ArrayList<>(); + List>> infoBroadcasts = new ArrayList<>(); + for (TransformMeta infoMeta : infoTransformMetas) { + Dataset infoDs = transformDatasetMap.get(infoMeta.getName()); + if (infoDs == null) { + throw new HopException( + "Unable to find Dataset for transform '" + + infoMeta.getName() + + "' providing info for '" + + transformMeta.getName() + + "'"); + } + IRowMeta infoRowMeta = pipelineMeta.getTransformFields(variables, infoMeta); + Broadcast> broadcast = + SparkInfoStreamSupport.broadcastInfoRows(spark, infoDs, infoRowMeta, infoMeta.getName()); + infoNames.add(infoMeta.getName()); + infoRowMetaJsons.add(JsonRowMeta.toJson(infoRowMeta)); + infoBroadcasts.add(broadcast); + log.logBasic( + "Broadcast info stream '" + + infoMeta.getName() + + "' → '" + + transformMeta.getName() + + "' rows=" + + broadcast.value().size()); + } + + IRowMeta outputRowMeta = pipelineMeta.getTransformFields(variables, transformMeta); + // Workflow Executor / Pipeline Executor only emit on named TARGET streams. + // getFields(next=null): WE clears the row; PE keeps the *input* layout (e.g. country_name). + // Prefer a non-empty target-stream layout so Dummy "results" gets ExecutionTime/… and the + // Spark Dataset schema matches hop rows written to those targets (Filter true/false share + // the same layout as main, so this is safe for multi-output field-preserving transforms). + if (!targetNames.isEmpty()) { + for (String targetName : targetNames) { + TransformMeta targetMeta = pipelineMeta.findTransform(targetName); + if (targetMeta == null) { + continue; + } + IRowMeta targetFields = + pipelineMeta.getTransformFields(variables, transformMeta, targetMeta, null); + if (targetFields != null && !targetFields.isEmpty()) { + if (outputRowMeta == null + || outputRowMeta.isEmpty() + || !sameFieldLayout(outputRowMeta, targetFields)) { + log.logBasic( + "Transform '" + + transformMeta.getName() + + "' using target stream '" + + targetName + + "' layout (" + + targetFields.size() + + " fields) for Spark Dataset schema" + + (outputRowMeta != null && !outputRowMeta.isEmpty() + ? " (main reported " + outputRowMeta.size() + " field(s))" + : "")); + outputRowMeta = targetFields; + } + break; + } + } + } + if (outputRowMeta == null) { + outputRowMeta = new org.apache.hop.core.row.RowMeta(); + } + StructType payloadSchema = HopSparkRowConverter.toStructType(outputRowMeta); + boolean multiTarget = !targetNames.isEmpty(); + StructType mapPartitionsSchema = + multiTarget ? HopSparkRowConverter.toTaggedStructType(outputRowMeta) : payloadSchema; + + String transformMetaXml = + XmlHandler.openTag(TransformMeta.XML_TAG) + + transformMeta.getTransform().getXml() + + XmlHandler.closeTag(TransformMeta.XML_TAG); + + List variableValues = collectVariables(variables); + String inputRowMetaJson = + rowMeta != null && rowMeta.size() > 0 ? JsonRowMeta.toJson(rowMeta) : null; + String outputRowMetaJson = JsonRowMeta.toJson(outputRowMeta); + + HopMapPartitionsFn mapFn = + new HopMapPartitionsFn( + variableValues, + metastoreJson, + transformMeta.getName(), + transformMeta.getTransformPluginId(), + transformMetaXml, + inputRowMetaJson, + outputRowMetaJson, + inputTransform, + targetNames, + infoNames, + infoRowMetaJsons, + infoBroadcasts, + metricsAccumulator, + sampleDataAccumulator, + runConfigName != null ? runConfigName : runConfigurationName, + parentLogChannelId, + dataSamplersJson); + + Dataset source; + if (inputTransform) { + // One empty row / one partition to drive source transforms (e.g. Row Generator) + // or pure multi-info transforms with no main hop + StructType emptySchema = + new StructType() + .add("_spark_hop_driver", org.apache.spark.sql.types.DataTypes.IntegerType, false); + List driverRows = new ArrayList<>(); + driverRows.add(RowFactory.create(1)); + source = spark.createDataFrame(driverRows, emptySchema).repartition(1); + } else { + source = input; + } + + SparkRunMode runMode = SparkRunMode.resolve(transformMeta, runConfiguration); + warnIfNestedSparkExecutorDistributed(log, transformMeta, runMode); + Dataset taggedOrPlain; + if (runMode == SparkRunMode.DRIVER_ONLY) { + taggedOrPlain = runOnDriver(log, transformMeta, spark, source, mapFn, mapPartitionsSchema); + } else { + JavaRDD outRdd = + source + .toJavaRDD() + .mapPartitions( + iterator -> { + try { + return mapFn.call(iterator); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + taggedOrPlain = spark.createDataFrame(outRdd, mapPartitionsSchema); + } + + if (!multiTarget) { + transformDatasetMap.put(transformMeta.getName(), taggedOrPlain); + } else { + // Main key may be empty when all rows are routed to named targets (Filter/Switch) + Dataset mainBranch = + taggedOrPlain + .filter( + taggedOrPlain + .col(HopSparkUtil.TARGET_TAG_COLUMN) + .equalTo(HopSparkUtil.MAIN_TARGET_TAG)) + .drop(HopSparkUtil.TARGET_TAG_COLUMN); + transformDatasetMap.put(transformMeta.getName(), mainBranch); + for (String targetName : targetNames) { + Dataset branch = + taggedOrPlain + .filter(taggedOrPlain.col(HopSparkUtil.TARGET_TAG_COLUMN).equalTo(targetName)) + .drop(HopSparkUtil.TARGET_TAG_COLUMN); + String tupleId = HopSparkUtil.createTargetTupleId(transformMeta.getName(), targetName); + transformDatasetMap.put(tupleId, branch); + log.logBasic( + "Target stream '" + + transformMeta.getName() + + "' → '" + + targetName + + "' key=" + + tupleId); + } + } + log.logBasic( + "Handled transform '" + + transformMeta.getName() + + "' with generic Spark " + + runMode.name() + + " (plugin id=" + + transformMeta.getTransformPluginId() + + (infoNames.isEmpty() ? "" : ", infoStreams=" + infoNames) + + (targetNames.isEmpty() ? "" : ", targets=" + targetNames) + + ")"); + } + + /** + * Pipeline Executor that starts a nested Native Spark engine must run on the driver. Log a clear + * warning when DISTRIBUTED would place that work on executors. + */ + static void warnIfNestedSparkExecutorDistributed( + ILogChannel log, TransformMeta transformMeta, SparkRunMode runMode) { + if (runMode == SparkRunMode.DRIVER_ONLY || transformMeta == null) { + return; + } + String pluginId = transformMeta.getTransformPluginId(); + if (!"PipelineExecutor".equals(pluginId)) { + return; + } + log.logBasic( + "Transform '" + + transformMeta.getName() + + "' is Pipeline Executor in DISTRIBUTED mode. Nested Native Spark child pipelines" + + " must run on the driver (set Generic transform run mode DRIVER_ONLY or use" + + " context action Spark Run Mode → Force Driver Only). DISTRIBUTED mapPartitions on" + + " executors cannot safely own a SparkSession."); + } + + /** + * Run {@link HopMapPartitionsFn} once on the Spark driver so nested executions (Workflow + * Executor, etc.) do not fan out across executor partitions. + */ + static Dataset runOnDriver( + ILogChannel log, + TransformMeta transformMeta, + SparkSession spark, + Dataset source, + HopMapPartitionsFn mapFn, + StructType mapPartitionsSchema) + throws HopException { + try { + List inputRows = source.collectAsList(); + if (inputRows.size() >= DRIVER_ONLY_ROW_WARN_THRESHOLD) { + log.logBasic( + "DRIVER_ONLY for transform '" + + transformMeta.getName() + + "': materializing " + + inputRows.size() + + " input rows on the Spark driver (threshold=" + + DRIVER_ONLY_ROW_WARN_THRESHOLD + + "). Prefer DISTRIBUTED for high-volume data."); + } + + Iterator outputIterator = mapFn.call(inputRows.iterator()); + List outputRows = new ArrayList<>(); + while (outputIterator.hasNext()) { + outputRows.add(outputIterator.next()); + } + log.logBasic( + "DRIVER_ONLY transform '" + + transformMeta.getName() + + "': driver processed inputRows=" + + inputRows.size() + + " outputRows=" + + outputRows.size()); + return spark.createDataFrame(outputRows, mapPartitionsSchema); + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException( + "Error running transform '" + + transformMeta.getName() + + "' in DRIVER_ONLY mode on the Spark driver", + e); + } + } + + /** + * Informational predecessors of {@code transformMeta} that are not already main previous + * transforms (Beam: allPrevious − mainPrevious). + */ + static List resolveInfoTransforms( + PipelineMeta pipelineMeta, + TransformMeta transformMeta, + List mainPreviousTransforms) { + List allPrevious = pipelineMeta.findPreviousTransforms(transformMeta, true); + if (allPrevious == null || allPrevious.isEmpty()) { + return List.of(); + } + Set mainNames = new HashSet<>(); + if (mainPreviousTransforms != null) { + for (TransformMeta main : mainPreviousTransforms) { + if (main != null) { + mainNames.add(main.getName()); + } + } + } + List info = new ArrayList<>(); + for (TransformMeta prev : allPrevious) { + if (prev != null && !mainNames.contains(prev.getName())) { + info.add(prev); + } + } + return info; + } + + /** + * Names of transforms bound as {@link IStream.StreamType#TARGET} streams (Filter true/false, + * Switch/Case cases, Pipeline/Workflow Executor result hops, …). Empty when the transform only + * has a default main hop. + */ + static List resolveTargetTransformNames(TransformMeta transformMeta) { + if (transformMeta == null || transformMeta.getTransform() == null) { + return List.of(); + } + ITransformIOMeta ioMeta = transformMeta.getTransform().getTransformIOMeta(); + if (ioMeta == null) { + return List.of(); + } + List names = new ArrayList<>(); + for (IStream targetStream : ioMeta.getTargetStreams()) { + if (targetStream != null && targetStream.getTransformMeta() != null) { + names.add(targetStream.getTransformMeta().getName()); + } + } + return names; + } + + /** Field names + types equal (order-sensitive). Used to detect PE main-vs-target layout drift. */ + static boolean sameFieldLayout(IRowMeta a, IRowMeta b) { + if (a == b) { + return true; + } + if (a == null || b == null || a.size() != b.size()) { + return false; + } + for (int i = 0; i < a.size(); i++) { + if (!a.getValueMeta(i).getName().equals(b.getValueMeta(i).getName()) + || a.getValueMeta(i).getType() != b.getValueMeta(i).getType()) { + return false; + } + } + return true; + } + + private static List collectVariables(IVariables variables) { + List list = new ArrayList<>(); + for (String name : variables.getVariableNames()) { + list.add(new SparkVariableValue(name, variables.getVariable(name))); + } + return list; + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkLakeTableInputHandler.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkLakeTableInputHandler.java new file mode 100644 index 00000000000..21212dd224d --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkLakeTableInputHandler.java @@ -0,0 +1,92 @@ +/* + * 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.hop.spark.pipeline.handler; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.core.SparkNativeMetrics; +import org.apache.hop.spark.engines.ISparkPipelineEngineRunConfiguration; +import org.apache.hop.spark.table.SparkLakeTableSupport; +import org.apache.hop.spark.transforms.table.SparkLakeTableInputMeta; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; + +/** Native Spark lake table read (Delta PATH in v1 / PR 2). */ +public class SparkLakeTableInputHandler extends SparkBaseTransformHandler { + + @Override + public boolean isInput() { + return true; + } + + @Override + public void handleTransform( + ILogChannel log, + IVariables variables, + String runConfigurationName, + ISparkPipelineEngineRunConfiguration runConfiguration, + IHopMetadataProvider metadataProvider, + String metastoreJson, + PipelineMeta pipelineMeta, + TransformMeta transformMeta, + Map> transformDatasetMap, + SparkSession spark, + IRowMeta rowMeta, + List previousTransforms, + Dataset input) + throws HopException { + + SparkLakeTableInputMeta meta = new SparkLakeTableInputMeta(); + loadTransformMetadata(meta, transformMeta, metadataProvider, pipelineMeta); + + String pathSchemeMap = runConfiguration != null ? runConfiguration.getPathSchemeMap() : null; + Dataset dataset = + SparkLakeTableSupport.resolveRead( + spark, variables, log, transformMeta.getName(), meta, pathSchemeMap); + dataset = trackMetrics(dataset, transformMeta, SparkNativeMetrics.Role.INPUT); + transformDatasetMap.put(transformMeta.getName(), dataset); + + String target = + SparkLakeTableInputMeta.MODE_TABLE.equalsIgnoreCase( + String.valueOf(meta.getIdentifierMode())) + ? "table=" + variables.resolve(meta.getTableIdentifier()) + : "path=" + variables.resolve(meta.getTablePath()); + log.logBasic( + "Handled Spark Lake Table Input : " + + transformMeta.getName() + + " format=" + + meta.getFormat() + + " mode=" + + meta.getIdentifierMode() + + " " + + target + + " timeTravel=" + + meta.getTimeTravelType() + + " columns=" + + Arrays.toString(dataset.columns())); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkLakeTableMaintenanceHandler.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkLakeTableMaintenanceHandler.java new file mode 100644 index 00000000000..a64f736fa4b --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkLakeTableMaintenanceHandler.java @@ -0,0 +1,153 @@ +/* + * 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.hop.spark.pipeline.handler; + +import java.util.List; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.engines.ISparkPipelineEngineRunConfiguration; +import org.apache.hop.spark.table.SparkLakeActionSupport; +import org.apache.hop.spark.table.SparkLakeTableSupport; +import org.apache.hop.spark.table.SparkLakeTableSupport.MaintenanceTarget; +import org.apache.hop.spark.table.SparkMaintenanceSqlBuilder; +import org.apache.hop.spark.transforms.table.SparkLakeTableMaintenanceMeta; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; + +/** + * Zero-input lakehouse maintenance action (KD-20). Upstream Dataset is ignored if present. Metrics + * are log-only. + */ +public class SparkLakeTableMaintenanceHandler extends SparkBaseTransformHandler { + + @Override + public boolean isOutput() { + return true; + } + + @Override + public void handleTransform( + ILogChannel log, + IVariables variables, + String runConfigurationName, + ISparkPipelineEngineRunConfiguration runConfiguration, + IHopMetadataProvider metadataProvider, + String metastoreJson, + PipelineMeta pipelineMeta, + TransformMeta transformMeta, + Map> transformDatasetMap, + SparkSession spark, + IRowMeta rowMeta, + List previousTransforms, + Dataset input) + throws HopException { + + // Zero-input: ignore upstream if hop-connected (KD-20) + SparkLakeTableMaintenanceMeta meta = new SparkLakeTableMaintenanceMeta(); + loadTransformMetadata(meta, transformMeta, metadataProvider, pipelineMeta); + + String operation = + StringUtils.isNotEmpty(meta.getOperation()) + ? variables.resolve(meta.getOperation()) + : meta.getOperation(); + + if (SparkMaintenanceSqlBuilder.requiresDestructiveAck(operation) + && !meta.isAcknowledgeDestructive()) { + throw new HopException( + "Spark Lake Table Maintenance '" + + transformMeta.getName() + + "' operation " + + operation + + " is destructive. Check 'I understand this may delete data files / snapshots'" + + " (acknowledgeDestructive) before running."); + } + + String pathSchemeMap = runConfiguration != null ? runConfiguration.getPathSchemeMap() : null; + MaintenanceTarget target = + SparkLakeTableSupport.resolveMaintenanceTarget( + spark, variables, meta, transformMeta.getName(), pathSchemeMap); + + String retention = + StringUtils.isNotEmpty(meta.getRetentionHours()) + ? variables.resolve(meta.getRetentionHours()) + : null; + String retainLast = + StringUtils.isNotEmpty(meta.getRetainLast()) + ? variables.resolve(meta.getRetainLast()) + : null; + String where = + StringUtils.isNotEmpty(meta.getWhereClause()) + ? variables.resolve(meta.getWhereClause()) + : null; + String zorder = + StringUtils.isNotEmpty(meta.getZOrderColumns()) + ? variables.resolve(meta.getZOrderColumns()) + : null; + + String sql = + SparkMaintenanceSqlBuilder.build( + target.format(), + target.targetSqlId(), + target.procedureCatalog(), + target.tableRefForCall(), + operation, + retention, + where, + zorder, + retainLast); + + try { + if (log != null) { + log.logBasic( + "Spark Lake Table Maintenance '" + + transformMeta.getName() + + "' op=" + + operation + + " target=" + + target.targetSqlId()); + log.logBasic("Maintenance SQL:\n" + sql); + } + spark.sql(sql); + if (log != null) { + log.logBasic( + "Spark Lake Table Maintenance '" + + transformMeta.getName() + + "' completed successfully"); + } + } catch (Exception e) { + throw new HopException( + "Maintenance failed in transform '" + + transformMeta.getName() + + "' (" + + operation + + "). SQL:\n" + + sql, + e); + } + + SparkLakeActionSupport.putEmptyLeaf(transformDatasetMap, transformMeta.getName(), spark); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkLakeTableMergeHandler.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkLakeTableMergeHandler.java new file mode 100644 index 00000000000..5ef6a7fc7a1 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkLakeTableMergeHandler.java @@ -0,0 +1,139 @@ +/* + * 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.hop.spark.pipeline.handler; + +import java.util.List; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.engines.ISparkPipelineEngineRunConfiguration; +import org.apache.hop.spark.table.SparkLakeActionSupport; +import org.apache.hop.spark.table.SparkLakeTableSupport; +import org.apache.hop.spark.table.SparkMergeSqlBuilder; +import org.apache.hop.spark.transforms.table.SparkLakeTableMergeMeta; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; + +/** + * Native Spark {@code MERGE INTO} against a Delta or Iceberg table. Single upstream Dataset is + * registered as a temp view; metrics are log-only (no reliable Dataset row counts for merge). + */ +public class SparkLakeTableMergeHandler extends SparkBaseTransformHandler { + + @Override + public boolean isOutput() { + return true; + } + + @Override + public void handleTransform( + ILogChannel log, + IVariables variables, + String runConfigurationName, + ISparkPipelineEngineRunConfiguration runConfiguration, + IHopMetadataProvider metadataProvider, + String metastoreJson, + PipelineMeta pipelineMeta, + TransformMeta transformMeta, + Map> transformDatasetMap, + SparkSession spark, + IRowMeta rowMeta, + List previousTransforms, + Dataset input) + throws HopException { + + if (input == null) { + throw new HopException( + "Spark Lake Table Merge '" + + transformMeta.getName() + + "' requires exactly one upstream Dataset (source rows for USING)."); + } + + SparkLakeTableMergeMeta meta = new SparkLakeTableMergeMeta(); + loadTransformMetadata(meta, transformMeta, metadataProvider, pipelineMeta); + + String pathSchemeMap = runConfiguration != null ? runConfiguration.getPathSchemeMap() : null; + String targetSqlId = + SparkLakeTableSupport.resolveMergeTargetSqlId( + spark, variables, meta, transformMeta.getName(), pathSchemeMap); + String sourceView = SparkMergeSqlBuilder.sourceViewName(transformMeta.getName()); + + String sql; + String raw = meta.getRawMergeSql(); + if (StringUtils.isNotEmpty(raw) && StringUtils.isNotBlank(variables.resolve(raw))) { + sql = variables.resolve(raw).trim(); + // Best-effort: if raw SQL references the default source view name, still register the view + } else { + String condition = + StringUtils.isNotEmpty(meta.getMergeCondition()) + ? variables.resolve(meta.getMergeCondition()) + : null; + sql = + SparkMergeSqlBuilder.build( + targetSqlId, + sourceView, + condition, + meta.getMatchedAction(), + meta.getNotMatchedAction(), + meta.getNotMatchedBySourceAction()); + } + + try { + input.createOrReplaceTempView(sourceView); + if (log != null) { + log.logBasic( + "Spark Lake Table Merge '" + + transformMeta.getName() + + "' source view=" + + sourceView + + " target=" + + targetSqlId); + log.logBasic("MERGE SQL:\n" + sql); + } + spark.sql(sql); + if (log != null) { + log.logBasic( + "Spark Lake Table Merge '" + transformMeta.getName() + "' completed successfully"); + } + } catch (Exception e) { + throw new HopException( + "MERGE failed in transform '" + + transformMeta.getName() + + "' against target " + + targetSqlId + + ". Ensure connectors and session extensions/catalogs are configured. SQL:\n" + + sql, + e); + } finally { + try { + spark.catalog().dropTempView(sourceView); + } catch (Exception ignored) { + // best effort cleanup + } + } + + SparkLakeActionSupport.putEmptyLeaf(transformDatasetMap, transformMeta.getName(), spark); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkLakeTableOutputHandler.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkLakeTableOutputHandler.java new file mode 100644 index 00000000000..94244fb350b --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkLakeTableOutputHandler.java @@ -0,0 +1,101 @@ +/* + * 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.hop.spark.pipeline.handler; + +import java.util.List; +import java.util.Map; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.core.SparkNativeMetrics; +import org.apache.hop.spark.engines.ISparkPipelineEngineRunConfiguration; +import org.apache.hop.spark.table.SparkLakeActionSupport; +import org.apache.hop.spark.table.SparkLakeTableSupport; +import org.apache.hop.spark.transforms.table.SparkLakeTableInputMeta; +import org.apache.hop.spark.transforms.table.SparkLakeTableOutputMeta; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; + +/** + * Native Spark lake table write (Delta PATH in v1 / PR 2). Executes the write as an action and + * registers an empty leaf Dataset. + */ +public class SparkLakeTableOutputHandler extends SparkBaseTransformHandler { + + @Override + public boolean isOutput() { + return true; + } + + @Override + public void handleTransform( + ILogChannel log, + IVariables variables, + String runConfigurationName, + ISparkPipelineEngineRunConfiguration runConfiguration, + IHopMetadataProvider metadataProvider, + String metastoreJson, + PipelineMeta pipelineMeta, + TransformMeta transformMeta, + Map> transformDatasetMap, + SparkSession spark, + IRowMeta rowMeta, + List previousTransforms, + Dataset input) + throws HopException { + + if (input == null) { + throw new HopException( + "Spark Lake Table Output '" + + transformMeta.getName() + + "' has no input Dataset. Connect an enabled hop from an upstream transform" + + " (disabled hops and disconnected transforms are not executed on native Spark)."); + } + + SparkLakeTableOutputMeta meta = new SparkLakeTableOutputMeta(); + loadTransformMetadata(meta, transformMeta, metadataProvider, pipelineMeta); + + Dataset toWrite = trackMetrics(input, transformMeta, SparkNativeMetrics.Role.OUTPUT); + String pathSchemeMap = runConfiguration != null ? runConfiguration.getPathSchemeMap() : null; + SparkLakeTableSupport.resolveWrite( + spark, toWrite, variables, transformMeta.getName(), meta, pathSchemeMap); + SparkLakeActionSupport.putEmptyLeaf(transformDatasetMap, transformMeta.getName(), spark); + + String target = + SparkLakeTableInputMeta.MODE_TABLE.equalsIgnoreCase( + String.valueOf(meta.getIdentifierMode())) + ? "table=" + variables.resolve(meta.getTableIdentifier()) + : "path=" + variables.resolve(meta.getTablePath()); + log.logBasic( + "Handled Spark Lake Table Output : " + + transformMeta.getName() + + " format=" + + meta.getFormat() + + " mode=" + + meta.getIdentifierMode() + + " saveMode=" + + meta.getSaveMode() + + " " + + target); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkMemoryGroupByHandler.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkMemoryGroupByHandler.java new file mode 100644 index 00000000000..ce13330c270 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkMemoryGroupByHandler.java @@ -0,0 +1,212 @@ +/* + * 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.hop.spark.pipeline.handler; + +import static org.apache.spark.sql.functions.avg; +import static org.apache.spark.sql.functions.col; +import static org.apache.spark.sql.functions.collect_list; +import static org.apache.spark.sql.functions.collect_set; +import static org.apache.spark.sql.functions.concat_ws; +import static org.apache.spark.sql.functions.count; +import static org.apache.spark.sql.functions.countDistinct; +import static org.apache.spark.sql.functions.first; +import static org.apache.spark.sql.functions.last; +import static org.apache.spark.sql.functions.lit; +import static org.apache.spark.sql.functions.max; +import static org.apache.spark.sql.functions.min; +import static org.apache.spark.sql.functions.percentile_approx; +import static org.apache.spark.sql.functions.stddev_pop; +import static org.apache.spark.sql.functions.sum; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.pipeline.transforms.memgroupby.GAggregate; +import org.apache.hop.pipeline.transforms.memgroupby.GGroup; +import org.apache.hop.pipeline.transforms.memgroupby.MemoryGroupByMeta; +import org.apache.hop.pipeline.transforms.memgroupby.MemoryGroupByMeta.GroupType; +import org.apache.hop.spark.core.SparkNativeMetrics; +import org.apache.hop.spark.engines.ISparkPipelineEngineRunConfiguration; +import org.apache.spark.sql.Column; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; + +/** + * Native Spark implementation of Memory Group By using Dataset groupBy + aggregate (global + * shuffle). + */ +public class SparkMemoryGroupByHandler extends SparkBaseTransformHandler { + + @Override + public void handleTransform( + ILogChannel log, + IVariables variables, + String runConfigurationName, + ISparkPipelineEngineRunConfiguration runConfiguration, + IHopMetadataProvider metadataProvider, + String metastoreJson, + PipelineMeta pipelineMeta, + TransformMeta transformMeta, + Map> transformDatasetMap, + SparkSession spark, + IRowMeta rowMeta, + List previousTransforms, + Dataset input) + throws HopException { + + if (input == null) { + throw new HopException( + "Memory Group By transform '" + transformMeta.getName() + "' has no input Dataset"); + } + + MemoryGroupByMeta meta = new MemoryGroupByMeta(); + loadTransformMetadata(meta, transformMeta, metadataProvider, pipelineMeta); + + List groupFields = new ArrayList<>(); + for (GGroup group : meta.getGroups()) { + if (StringUtils.isNotEmpty(group.getField())) { + groupFields.add(group.getField()); + } + } + + List aggCols = new ArrayList<>(); + for (GAggregate aggregate : meta.getAggregates()) { + aggCols.add(toSparkAgg(aggregate).alias(aggregate.getField())); + } + + Dataset output; + if (groupFields.isEmpty()) { + // Global aggregation (single group) + if (aggCols.isEmpty()) { + throw new HopException( + "Memory Group By '" + transformMeta.getName() + "' has no group or aggregate fields"); + } + Column[] aggs = aggCols.toArray(new Column[0]); + if (aggs.length == 1) { + output = input.agg(aggs[0]); + } else { + output = input.agg(aggs[0], java.util.Arrays.copyOfRange(aggs, 1, aggs.length)); + } + } else { + Column[] groupCols = groupFields.stream().map(f -> col(f)).toArray(Column[]::new); + if (aggCols.isEmpty()) { + // Distinct groups only + output = input.select(groupCols).distinct(); + } else { + Column[] aggs = aggCols.toArray(new Column[0]); + if (aggs.length == 1) { + output = input.groupBy(groupCols).agg(aggs[0]); + } else { + output = + input + .groupBy(groupCols) + .agg(aggs[0], java.util.Arrays.copyOfRange(aggs, 1, aggs.length)); + } + } + } + + // Reorder / project to Hop output field order when available + IRowMeta outputMeta = pipelineMeta.getTransformFields(variables, transformMeta); + if (outputMeta != null && outputMeta.size() > 0) { + List select = new ArrayList<>(); + for (int i = 0; i < outputMeta.size(); i++) { + String name = outputMeta.getValueMeta(i).getName(); + if (java.util.Arrays.asList(output.columns()).contains(name)) { + select.add(col(name)); + } + } + if (!select.isEmpty()) { + output = output.select(select.toArray(new Column[0])); + } + } + + output = trackMetrics(output, transformMeta, SparkNativeMetrics.Role.TRANSFORM); + transformDatasetMap.put(transformMeta.getName(), output); + log.logBasic( + "Handled Memory Group By (native Spark) : " + + transformMeta.getName() + + " groups=" + + groupFields + + " aggregates=" + + meta.getAggregates().size()); + } + + static Column toSparkAgg(GAggregate aggregate) throws HopException { + GroupType type = aggregate.getType(); + if (type == null || type == GroupType.None) { + throw new HopException("Aggregate type is not set for field '" + aggregate.getField() + "'"); + } + String subject = aggregate.getSubject(); + String valueField = aggregate.getValueField(); + + return switch (type) { + case Sum -> sum(col(subject)); + case Average -> avg(col(subject)); + case Minimum -> min(col(subject)); + case Maximum -> max(col(subject)); + case CountAll -> count(col(subject)); + case CountAny -> count(lit(1)); + case CountDistinct -> countDistinct(col(subject)); + case First -> first(col(subject), true); + case FirstIncludingNull -> first(col(subject), false); + case Last -> last(col(subject), true); + case LastIncludingNull -> last(col(subject), false); + case StandardDeviation -> stddev_pop(col(subject)); + case Median -> percentile_approx(col(subject), lit(0.5), lit(10000)); + case Percentile -> { + double p = 0.5; + if (StringUtils.isNotEmpty(valueField)) { + try { + p = Double.parseDouble(valueField) / 100.0; + } catch (NumberFormatException e) { + throw new HopException( + "Invalid percentile value '" + + valueField + + "' for aggregate " + + aggregate.getField(), + e); + } + } + yield percentile_approx(col(subject), lit(p), lit(10000)); + } + case ConcatComma -> concat_ws(",", collect_list(col(subject))); + case ConcatString -> { + String sep = StringUtils.isNotEmpty(valueField) ? valueField : ""; + yield concat_ws(sep, collect_list(col(subject))); + } + case ConcatDistinct -> { + String sep = StringUtils.isNotEmpty(valueField) ? valueField : ","; + yield concat_ws(sep, collect_set(col(subject))); + } + default -> + throw new HopException( + "Aggregate type '" + + type.getCode() + + "' is not supported by the native Spark Memory Group By handler"); + }; + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkMergeJoinHandler.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkMergeJoinHandler.java new file mode 100644 index 00000000000..798df41d5e8 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkMergeJoinHandler.java @@ -0,0 +1,202 @@ +/* + * 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.hop.spark.pipeline.handler; + +import static org.apache.spark.sql.functions.col; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.pipeline.transforms.mergejoin.MergeJoinMeta; +import org.apache.hop.spark.core.SparkNativeMetrics; +import org.apache.hop.spark.engines.ISparkPipelineEngineRunConfiguration; +import org.apache.spark.sql.Column; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; + +/** Native Spark Dataset join for the Merge Join transform. */ +public class SparkMergeJoinHandler extends SparkBaseTransformHandler { + + private static final String RIGHT_PREFIX = "__spark_r__"; + + @Override + public void handleTransform( + ILogChannel log, + IVariables variables, + String runConfigurationName, + ISparkPipelineEngineRunConfiguration runConfiguration, + IHopMetadataProvider metadataProvider, + String metastoreJson, + PipelineMeta pipelineMeta, + TransformMeta transformMeta, + Map> transformDatasetMap, + SparkSession spark, + IRowMeta rowMeta, + List previousTransforms, + Dataset input) + throws HopException { + + MergeJoinMeta meta = new MergeJoinMeta(); + loadTransformMetadata(meta, transformMeta, metadataProvider, pipelineMeta); + + String leftName = resolveLeftTransformName(meta, previousTransforms); + String rightName = resolveRightTransformName(meta, previousTransforms); + + Dataset left = transformDatasetMap.get(leftName); + Dataset right = transformDatasetMap.get(rightName); + if (left == null) { + throw new HopException( + "Merge Join '" + + transformMeta.getName() + + "': left Dataset for transform '" + + leftName + + "' not found"); + } + if (right == null) { + throw new HopException( + "Merge Join '" + + transformMeta.getName() + + "': right Dataset for transform '" + + rightName + + "' not found"); + } + + List leftKeys = meta.getKeyFields1(); + List rightKeys = meta.getKeyFields2(); + if (leftKeys == null || rightKeys == null || leftKeys.isEmpty() || rightKeys.isEmpty()) { + throw new HopException( + "Merge Join '" + transformMeta.getName() + "' requires key fields on both sides"); + } + if (leftKeys.size() != rightKeys.size()) { + throw new HopException( + "Merge Join '" + + transformMeta.getName() + + "' key field counts differ: left=" + + leftKeys.size() + + " right=" + + rightKeys.size()); + } + + // Prefix right columns to avoid name collisions after join + Dataset rightPrefixed = right; + for (String column : right.columns()) { + rightPrefixed = rightPrefixed.withColumnRenamed(column, RIGHT_PREFIX + column); + } + + Column joinCond = null; + for (int i = 0; i < leftKeys.size(); i++) { + Column part = + left.col(leftKeys.get(i)).equalTo(rightPrefixed.col(RIGHT_PREFIX + rightKeys.get(i))); + joinCond = joinCond == null ? part : joinCond.and(part); + } + + String sparkJoinType = toSparkJoinType(meta.getJoinType()); + Dataset joined = left.join(rightPrefixed, joinCond, sparkJoinType); + + // Build output columns: all left columns + right columns (stripped prefix). + // For duplicate names, keep left and rename right with _1 style only if needed. + Set usedNames = new HashSet<>(Arrays.asList(left.columns())); + List selectCols = new ArrayList<>(); + for (String c : left.columns()) { + selectCols.add(col(c)); + } + for (String c : right.columns()) { + String prefixed = RIGHT_PREFIX + c; + String outName = c; + if (usedNames.contains(outName)) { + outName = c + "_1"; + int n = 1; + while (usedNames.contains(outName)) { + n++; + outName = c + "_" + n; + } + } + usedNames.add(outName); + selectCols.add(col(prefixed).alias(outName)); + } + + Dataset output = joined.select(selectCols.toArray(new Column[0])); + output = trackMetrics(output, transformMeta, SparkNativeMetrics.Role.TRANSFORM); + transformDatasetMap.put(transformMeta.getName(), output); + log.logBasic( + "Handled Merge Join (native Spark) : " + + transformMeta.getName() + + " type=" + + meta.getJoinType() + + " left=" + + leftName + + " right=" + + rightName); + } + + private static String toSparkJoinType(String hopJoinType) throws HopException { + if (StringUtils.isEmpty(hopJoinType)) { + throw new HopException("Merge Join type is not set"); + } + return switch (hopJoinType.trim().toUpperCase()) { + case "INNER" -> "inner"; + case "LEFT OUTER" -> "left_outer"; + case "RIGHT OUTER" -> "right_outer"; + case "FULL OUTER" -> "full_outer"; + default -> + throw new HopException("Join type '" + hopJoinType + "' is not recognized or supported"); + }; + } + + private static String resolveLeftTransformName( + MergeJoinMeta meta, List previousTransforms) throws HopException { + if (StringUtils.isNotEmpty(meta.getLeftTransformName())) { + return meta.getLeftTransformName(); + } + if (meta.getTransformIOMeta().getInfoStreams().size() >= 1 + && meta.getTransformIOMeta().getInfoStreams().get(0).getTransformMeta() != null) { + return meta.getTransformIOMeta().getInfoStreams().get(0).getTransformMeta().getName(); + } + if (previousTransforms != null && !previousTransforms.isEmpty()) { + return previousTransforms.get(0).getName(); + } + throw new HopException("Unable to resolve left transform for Merge Join"); + } + + private static String resolveRightTransformName( + MergeJoinMeta meta, List previousTransforms) throws HopException { + if (StringUtils.isNotEmpty(meta.getRightTransformName())) { + return meta.getRightTransformName(); + } + if (meta.getTransformIOMeta().getInfoStreams().size() >= 2 + && meta.getTransformIOMeta().getInfoStreams().get(1).getTransformMeta() != null) { + return meta.getTransformIOMeta().getInfoStreams().get(1).getTransformMeta().getName(); + } + if (previousTransforms != null && previousTransforms.size() >= 2) { + return previousTransforms.get(1).getName(); + } + throw new HopException("Unable to resolve right transform for Merge Join"); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkSortRowsHandler.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkSortRowsHandler.java new file mode 100644 index 00000000000..b4d5af9c68d --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkSortRowsHandler.java @@ -0,0 +1,116 @@ +/* + * 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.hop.spark.pipeline.handler; + +import static org.apache.spark.sql.functions.col; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.pipeline.transforms.sort.SortRowsField; +import org.apache.hop.pipeline.transforms.sort.SortRowsMeta; +import org.apache.hop.spark.core.SparkNativeMetrics; +import org.apache.hop.spark.engines.ISparkPipelineEngineRunConfiguration; +import org.apache.spark.sql.Column; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; + +/** Native Spark global sort (orderBy) for Sort Rows. */ +public class SparkSortRowsHandler extends SparkBaseTransformHandler { + + @Override + public void handleTransform( + ILogChannel log, + IVariables variables, + String runConfigurationName, + ISparkPipelineEngineRunConfiguration runConfiguration, + IHopMetadataProvider metadataProvider, + String metastoreJson, + PipelineMeta pipelineMeta, + TransformMeta transformMeta, + Map> transformDatasetMap, + SparkSession spark, + IRowMeta rowMeta, + List previousTransforms, + Dataset input) + throws HopException { + + if (input == null) { + throw new HopException( + "Sort Rows transform '" + transformMeta.getName() + "' has no input Dataset"); + } + + SortRowsMeta meta = new SortRowsMeta(); + loadTransformMetadata(meta, transformMeta, metadataProvider, pipelineMeta); + + List sortFields = meta.getSortFields(); + if (sortFields == null || sortFields.isEmpty()) { + throw new HopException( + "Sort Rows '" + transformMeta.getName() + "' has no sort fields configured"); + } + + List orderCols = new ArrayList<>(); + for (SortRowsField field : sortFields) { + if (StringUtils.isEmpty(field.getFieldName())) { + continue; + } + Column c = col(field.getFieldName()); + // Hop: ascending=true means ASC; false means DESC + orderCols.add(field.isAscending() ? c.asc() : c.desc()); + } + if (orderCols.isEmpty()) { + throw new HopException( + "Sort Rows '" + transformMeta.getName() + "' has no valid sort field names"); + } + + Dataset output = input.orderBy(orderCols.toArray(new Column[0])); + + if (meta.isOnlyPassingUniqueRows()) { + // Unique on the sort keys (Hop behavior approximates sorted unique) + String[] keys = + sortFields.stream() + .map(SortRowsField::getFieldName) + .filter(StringUtils::isNotEmpty) + .toArray(String[]::new); + if (keys.length > 0) { + output = output.dropDuplicates(keys); + } else { + output = output.dropDuplicates(); + } + } + + output = trackMetrics(output, transformMeta, SparkNativeMetrics.Role.TRANSFORM); + transformDatasetMap.put(transformMeta.getName(), output); + log.logBasic( + "Handled Sort Rows (native Spark) : " + + transformMeta.getName() + + " fields=" + + sortFields.size() + + " unique=" + + meta.isOnlyPassingUniqueRows()); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkUniqueRowsHandler.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkUniqueRowsHandler.java new file mode 100644 index 00000000000..837443801b1 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pipeline/handler/SparkUniqueRowsHandler.java @@ -0,0 +1,177 @@ +/* + * 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.hop.spark.pipeline.handler; + +import static org.apache.spark.sql.functions.col; +import static org.apache.spark.sql.functions.count; +import static org.apache.spark.sql.functions.lower; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.pipeline.transforms.uniquerows.UniqueField; +import org.apache.hop.pipeline.transforms.uniquerows.UniqueRowsMeta; +import org.apache.hop.spark.core.SparkNativeMetrics; +import org.apache.hop.spark.engines.ISparkPipelineEngineRunConfiguration; +import org.apache.spark.sql.Column; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; + +/** + * Native Spark distinct / dropDuplicates for Unique Rows. Count of duplicates is supported via + * groupBy; error-handling reject mode is not supported. + */ +public class SparkUniqueRowsHandler extends SparkBaseTransformHandler { + + @Override + public void handleTransform( + ILogChannel log, + IVariables variables, + String runConfigurationName, + ISparkPipelineEngineRunConfiguration runConfiguration, + IHopMetadataProvider metadataProvider, + String metastoreJson, + PipelineMeta pipelineMeta, + TransformMeta transformMeta, + Map> transformDatasetMap, + SparkSession spark, + IRowMeta rowMeta, + List previousTransforms, + Dataset input) + throws HopException { + + if (input == null) { + throw new HopException( + "Unique Rows transform '" + transformMeta.getName() + "' has no input Dataset"); + } + + UniqueRowsMeta meta = new UniqueRowsMeta(); + loadTransformMetadata(meta, transformMeta, metadataProvider, pipelineMeta); + + if (meta.isRejectDuplicateRow()) { + throw new HopException( + "Unique Rows '" + + transformMeta.getName() + + "': reject duplicate rows (error handling) is not supported on the native Spark engine"); + } + + List compareFields = meta.getCompareFields(); + List keyNames = new ArrayList<>(); + if (compareFields != null) { + for (UniqueField field : compareFields) { + if (StringUtils.isNotEmpty(field.getName())) { + keyNames.add(field.getName()); + } + } + } + + Dataset output; + if (meta.isCountRows()) { + output = distinctWithCount(input, meta, keyNames, rowMeta); + } else if (keyNames.isEmpty()) { + output = input.dropDuplicates(); + } else if (hasCaseInsensitive(compareFields)) { + output = dropDuplicatesCaseInsensitive(input, compareFields, keyNames); + } else { + output = input.dropDuplicates(keyNames.toArray(new String[0])); + } + + output = trackMetrics(output, transformMeta, SparkNativeMetrics.Role.TRANSFORM); + transformDatasetMap.put(transformMeta.getName(), output); + log.logBasic( + "Handled Unique Rows (native Spark) : " + + transformMeta.getName() + + " keys=" + + (keyNames.isEmpty() ? "(all columns)" : keyNames) + + " countRows=" + + meta.isCountRows()); + } + + private static boolean hasCaseInsensitive(List fields) { + if (fields == null) { + return false; + } + for (UniqueField f : fields) { + if (f.isCaseInsensitive()) { + return true; + } + } + return false; + } + + private static Dataset dropDuplicatesCaseInsensitive( + Dataset input, List compareFields, List keyNames) { + // Normalize case-insensitive keys into temp columns, dropDuplicates, drop temps + Dataset work = input; + List dedupeKeys = new ArrayList<>(); + List tempCols = new ArrayList<>(); + for (UniqueField field : compareFields) { + if (StringUtils.isEmpty(field.getName())) { + continue; + } + if (field.isCaseInsensitive()) { + String temp = "__ci__" + field.getName(); + work = work.withColumn(temp, lower(col(field.getName()))); + dedupeKeys.add(temp); + tempCols.add(temp); + } else { + dedupeKeys.add(field.getName()); + } + } + work = work.dropDuplicates(dedupeKeys.toArray(new String[0])); + for (String temp : tempCols) { + work = work.drop(temp); + } + return work; + } + + private static Dataset distinctWithCount( + Dataset input, UniqueRowsMeta meta, List keyNames, IRowMeta rowMeta) + throws HopException { + String countField = + StringUtils.isNotEmpty(meta.getCountField()) ? meta.getCountField() : "count"; + + if (keyNames.isEmpty()) { + // Count duplicates across entire row + if (rowMeta == null || rowMeta.size() == 0) { + throw new HopException("Unique Rows with count requires known input fields"); + } + for (int i = 0; i < rowMeta.size(); i++) { + keyNames.add(rowMeta.getValueMeta(i).getName()); + } + } + + Column[] groupCols = keyNames.stream().map(f -> col(f)).toArray(Column[]::new); + // Hop unique with count keeps one full row + the number of occurrences. + Dataset counts = + input + .groupBy(groupCols) + .agg(count(org.apache.spark.sql.functions.lit(1)).alias(countField)); + Dataset uniqueRows = input.dropDuplicates(keyNames.toArray(new String[0])); + return uniqueRows.join(counts, keyNames.toArray(new String[0]), "inner"); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/pkg/PackageExportFilter.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pkg/PackageExportFilter.java new file mode 100644 index 00000000000..5d1c80e0d2f --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pkg/PackageExportFilter.java @@ -0,0 +1,334 @@ +/* + * 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.hop.spark.pkg; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.FileSystems; +import java.nio.file.PathMatcher; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.vfs2.FileObject; +import org.apache.commons.vfs2.FileType; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.json.HopJson; +import org.apache.hop.core.vfs.HopVfs; + +/** + * Include/exclude rules for {@link SparkProjectPackage#exportProject}. Built-in defaults skip + * common bulk/IDE dirs ({@code work}, {@code datasets}, {@code target}, …). Project file {@code + * spark-package.json} and CLI/GUI options merge on top. + * + *

Not the same as {@code .gitignore}. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public final class PackageExportFilter { + + /** Project-home config filename (preferred). */ + public static final String CONFIG_FILENAME = "spark-package.json"; + + /** Built-in directory basenames always skipped unless {@link #replaceDefaultExcludeDirs}. */ + public static final Set DEFAULT_EXCLUDE_DIRS = + Set.of(".git", "datasets", "target", "node_modules", ".idea", ".settings", "work"); + + @JsonProperty("excludeDirs") + private List excludeDirs = new ArrayList<>(); + + @JsonProperty("excludeGlobs") + private List excludeGlobs = new ArrayList<>(); + + @JsonProperty("includePaths") + private List includePaths = new ArrayList<>(); + + @JsonProperty("replaceDefaultExcludeDirs") + private boolean replaceDefaultExcludeDirs; + + public PackageExportFilter() {} + + public PackageExportFilter( + List excludeDirs, + List excludeGlobs, + List includePaths, + boolean replaceDefaultExcludeDirs) { + this.excludeDirs = excludeDirs != null ? new ArrayList<>(excludeDirs) : new ArrayList<>(); + this.excludeGlobs = excludeGlobs != null ? new ArrayList<>(excludeGlobs) : new ArrayList<>(); + this.includePaths = includePaths != null ? new ArrayList<>(includePaths) : new ArrayList<>(); + this.replaceDefaultExcludeDirs = replaceDefaultExcludeDirs; + } + + public static PackageExportFilter empty() { + return new PackageExportFilter(); + } + + public List getExcludeDirs() { + return excludeDirs; + } + + public void setExcludeDirs(List excludeDirs) { + this.excludeDirs = excludeDirs != null ? excludeDirs : new ArrayList<>(); + } + + public List getExcludeGlobs() { + return excludeGlobs; + } + + public void setExcludeGlobs(List excludeGlobs) { + this.excludeGlobs = excludeGlobs != null ? excludeGlobs : new ArrayList<>(); + } + + public List getIncludePaths() { + return includePaths; + } + + public void setIncludePaths(List includePaths) { + this.includePaths = includePaths != null ? includePaths : new ArrayList<>(); + } + + public boolean isReplaceDefaultExcludeDirs() { + return replaceDefaultExcludeDirs; + } + + public void setReplaceDefaultExcludeDirs(boolean replaceDefaultExcludeDirs) { + this.replaceDefaultExcludeDirs = replaceDefaultExcludeDirs; + } + + /** + * Effective directory basenames to skip (lowercase). When {@link #replaceDefaultExcludeDirs} is + * false, starts from {@link #DEFAULT_EXCLUDE_DIRS} then adds {@link #excludeDirs}. + */ + @JsonIgnore + public Set effectiveExcludeDirs() { + LinkedHashSet dirs = new LinkedHashSet<>(); + if (!replaceDefaultExcludeDirs) { + for (String d : DEFAULT_EXCLUDE_DIRS) { + dirs.add(d.toLowerCase(Locale.ROOT)); + } + } + for (String d : excludeDirs) { + if (StringUtils.isNotBlank(d)) { + dirs.add(d.trim().toLowerCase(Locale.ROOT)); + } + } + return Collections.unmodifiableSet(dirs); + } + + /** + * Merge another filter on top of this one. Lists append; if {@code + * other.replaceDefaultExcludeDirs} is true, that flag is set and default dirs are not used when + * computing {@link #effectiveExcludeDirs()} after merge (other's excludeDirs replace the combined + * list for the replace semantics: we set replace flag and use only other's dirs + this's dirs if + * needed). + * + *

Merge order intended: {@code defaults-as-empty.merge(file).merge(cli)}. + */ + public PackageExportFilter merge(PackageExportFilter other) { + if (other == null) { + return copy(); + } + PackageExportFilter out = copy(); + if (other.replaceDefaultExcludeDirs) { + out.replaceDefaultExcludeDirs = true; + out.excludeDirs = new ArrayList<>(other.excludeDirs); + } else { + LinkedHashSet dirs = new LinkedHashSet<>(out.excludeDirs); + dirs.addAll(other.excludeDirs); + out.excludeDirs = new ArrayList<>(dirs); + } + LinkedHashSet globs = new LinkedHashSet<>(out.excludeGlobs); + globs.addAll(other.excludeGlobs); + out.excludeGlobs = new ArrayList<>(globs); + LinkedHashSet includes = new LinkedHashSet<>(out.includePaths); + includes.addAll(other.includePaths); + out.includePaths = new ArrayList<>(includes); + return out; + } + + public PackageExportFilter copy() { + return new PackageExportFilter( + excludeDirs, excludeGlobs, includePaths, replaceDefaultExcludeDirs); + } + + /** + * Whether a project-relative path should be excluded from the package. + * + * @param relativePath path using {@code /} separators, relative to project home + */ + public boolean shouldSkipRelative(String relativePath) { + if (StringUtils.isEmpty(relativePath)) { + return true; + } + String rel = relativePath.replace('\\', '/'); + while (rel.startsWith("./")) { + rel = rel.substring(2); + } + if (isIncluded(rel)) { + return false; + } + String[] parts = rel.split("/"); + Set skipDirs = effectiveExcludeDirs(); + for (String part : parts) { + if (part.startsWith(".") && part.length() > 1) { + return true; + } + if (skipDirs.contains(part.toLowerCase(Locale.ROOT))) { + return true; + } + } + for (String pattern : excludeGlobs) { + if (StringUtils.isBlank(pattern)) { + continue; + } + try { + PathMatcher matcher = + FileSystems.getDefault().getPathMatcher("glob:" + pattern.trim().replace('\\', '/')); + // PathMatcher needs a Path; use relative path as a pseudo-path + if (matcher.matches(java.nio.file.Path.of(rel))) { + return true; + } + } catch (Exception ignored) { + // invalid glob: treat as no match + } + } + return false; + } + + /** True if folder basename should not be traversed (unless an include lives under it). */ + public boolean shouldSkipDirectoryBasename(String basename, int depth) { + if (StringUtils.isEmpty(basename)) { + return false; + } + if (basename.startsWith(".") && depth > 0) { + return true; + } + if (effectiveExcludeDirs().contains(basename.toLowerCase(Locale.ROOT))) { + // Still traverse if any includePaths live under this directory name as a segment + for (String inc : includePaths) { + if (StringUtils.isBlank(inc)) { + continue; + } + String n = inc.replace('\\', '/'); + if (n.equals(basename) + || n.startsWith(basename + "/") + || n.contains("/" + basename + "/")) { + return false; + } + } + return true; + } + return false; + } + + private boolean isIncluded(String rel) { + for (String raw : includePaths) { + if (StringUtils.isBlank(raw)) { + continue; + } + String inc = raw.trim().replace('\\', '/'); + while (inc.startsWith("./")) { + inc = inc.substring(2); + } + if (rel.equals(inc) || rel.startsWith(inc.endsWith("/") ? inc : inc + "/")) { + return true; + } + } + return false; + } + + /** Load {@link #CONFIG_FILENAME} from project home, or empty filter if missing. */ + public static PackageExportFilter loadFromProjectHome(String projectHome) throws HopException { + if (StringUtils.isEmpty(projectHome)) { + return empty(); + } + String path = projectHome; + if (!path.endsWith("/") && !path.endsWith("\\")) { + path = path + "/"; + } + path = path + CONFIG_FILENAME; + try { + FileObject fo = HopVfs.getFileObject(path); + if (!fo.exists() || fo.getType() != FileType.FILE) { + return empty(); + } + try (InputStream in = HopVfs.getInputStream(fo)) { + ObjectMapper mapper = HopJson.newMapper(); + PackageExportFilter f = mapper.readValue(in, PackageExportFilter.class); + return f != null ? f : empty(); + } + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException( + "Unable to read Spark package filter config '" + path + "': " + e.getMessage(), e); + } + } + + /** Write this filter to {@code projectHome/spark-package.json}. */ + public static void saveToProjectHome(String projectHome, PackageExportFilter filter) + throws HopException { + if (StringUtils.isEmpty(projectHome)) { + throw new HopException("Project home is required to save " + CONFIG_FILENAME); + } + if (filter == null) { + filter = empty(); + } + String path = projectHome; + if (!path.endsWith("/") && !path.endsWith("\\")) { + path = path + "/"; + } + path = path + CONFIG_FILENAME; + try { + FileObject fo = HopVfs.getFileObject(path); + ObjectMapper mapper = HopJson.newMapper(); + mapper.enable(SerializationFeature.INDENT_OUTPUT); + try (OutputStream out = HopVfs.getOutputStream(fo, false)) { + mapper.writeValue(out, filter); + } + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException( + "Unable to write Spark package filter config '" + path + "': " + e.getMessage(), e); + } + } + + /** Parse comma-separated list for CLI. */ + public static List splitCsv(String csv) { + if (StringUtils.isBlank(csv)) { + return List.of(); + } + List out = new ArrayList<>(); + for (String p : csv.split(",")) { + if (StringUtils.isNotBlank(p)) { + out.add(p.trim()); + } + } + return out; + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/pkg/SparkProjectPackage.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pkg/SparkProjectPackage.java new file mode 100644 index 00000000000..d0b8dc4f7e3 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/pkg/SparkProjectPackage.java @@ -0,0 +1,1021 @@ +/* + * 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.hop.spark.pkg; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.util.HashSet; +import java.util.HexFormat; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.vfs2.FileObject; +import org.apache.commons.vfs2.FileSelectInfo; +import org.apache.commons.vfs2.FileSelector; +import org.apache.commons.vfs2.FileType; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.metadata.SerializableMetadataProvider; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.core.vfs.HopVfs; +import org.apache.hop.metadata.api.IHopMetadataProvider; + +/** + * Native Spark project package: a zip of Hop definition files (pipelines, workflows, small + * resources) plus {@code metadata.json}, staged for cluster/local Spark runs so nested Simple + * Mapping / Pipeline Executor paths under {@code ${PROJECT_HOME}} resolve via Hop VFS. + * + *

Layout written by {@link #exportProject}: + * + *

+ * metadata.json
+ * hop-spark-package.properties   # projectRoot=project
+ * project/…                      # project home tree (relative paths)
+ * 
+ * + *

Also accepts classic GUI project export zips (single top-level folder). Default + * materialization extracts into {@code ${java.io.tmpdir}/hop-spark-pkg-<key>/}. Extracts are + * reused only while the package zip fingerprint (size, mtime, sha-256) still matches — so + * overwriting the same path on a shared volume is picked up on the next materialize. + * + *

Not for bulk Dataset data. {@code PROJECT_HOME} after materialize points at + * definition files. Use separate variables ({@code HOP_DATA}, {@code OUTPUT_ROOT}, {@code s3a://…}) + * for Spark File / Lake paths. + */ +public final class SparkProjectPackage { + + /** + * Bumped when worker package distribution behavior changes. Printed by MainSpark so Databricks + * logs prove which fat jar is live (avoid debugging stale Volume jars). + */ + public static final String DISTRIBUTION_BUILD_ID = "pkg-dist-v3-stage-volumes-for-addfile"; + + /** Variable holding the package zip URI/path for driver and executor materialization. */ + public static final String VAR_PACKAGE_URI = "HOP_SPARK_PROJECT_PACKAGE"; + + /** + * Basename of the package after {@link org.apache.spark.SparkContext#addFile(String)} — workers + * resolve it via {@link org.apache.spark.SparkFiles#get(String)}. + */ + public static final String VAR_PACKAGE_SPARK_FILE = "HOP_SPARK_PROJECT_PACKAGE_SPARK_FILE"; + + public static final String METADATA_ENTRY = "metadata.json"; + public static final String MARKER_ENTRY = "hop-spark-package.properties"; + public static final String DEFAULT_PROJECT_ROOT = "project"; + public static final String PROP_PROJECT_ROOT = "projectRoot"; + + private static final String COMPLETE_MARKER = ".hop-spark-package-complete"; + + /** + * packageUri cache key → extract root + fingerprint that was materialized (in-JVM). Disk marker + * under the extract root carries the same fingerprint for long-lived worker JVMs. + */ + private static final Map MATERIALIZED = new ConcurrentHashMap<>(); + + /** applicationId|localPath keys already passed to SparkContext.addFile on this driver JVM. */ + private static final Set DISTRIBUTED = ConcurrentHashMap.newKeySet(); + + private record MaterializedState(String extractRoot, String fingerprint) {} + + private SparkProjectPackage() {} + + /** Result of materializing a package zip on this JVM. */ + public record Materialized(String projectHome, String metadataPath, String extractRoot) {} + + /** + * Ensure the project package (if configured) is extracted and {@code PROJECT_HOME} points at the + * project root. Safe to call from every mapPartitions init; materializes once per package path + * per JVM. + * + *

Tries each candidate from {@link #listPackagePathsForMaterialize} until one materializes. + * Never keeps a driver-only {@code PROJECT_HOME} under another node's {@code /tmp}. Never stores + * SparkFiles paths back into {@link #VAR_PACKAGE_URI} (those are per-JVM and not portable). + */ + public static void ensureMaterializedOnWorker(IVariables variables) throws HopException { + if (variables == null) { + return; + } + String preservedUri = variables.getVariable(VAR_PACKAGE_URI); + String sparkFile = variables.getVariable(VAR_PACKAGE_SPARK_FILE); + + // Drop driver-broadcast PROJECT_HOME when it is not a real directory on *this* JVM. + clearStaleProjectHome(variables); + + java.util.List candidates = listPackagePathsForMaterialize(variables); + if (candidates.isEmpty()) { + if (StringUtils.isNotEmpty(preservedUri)) { + throw new HopException( + "Could not resolve Spark project package on this host for materialization: " + + variables.resolve(preservedUri) + + " (sparkFile=" + + sparkFile + + "). PROJECT_HOME was " + + variables.getVariable("PROJECT_HOME") + + ". On Databricks Deploy & run, keep the package on a UC Volume (/Volumes/…) and" + + " ensure the fat jar stages a SparkFiles copy for executors that cannot open" + + " Volumes via java.io.File / Hop VFS."); + } + return; + } + + HopException lastError = null; + Materialized m = null; + String usedPath = null; + for (String packagePath : candidates) { + try { + m = materialize(packagePath); + usedPath = packagePath; + break; + } catch (HopException e) { + lastError = e; + } catch (Exception e) { + lastError = + new HopException("Error materializing Spark project package: " + packagePath, e); + } + } + if (m == null) { + throw new HopException( + "Could not materialize Spark project package from any candidate path: " + + candidates + + ". Last error: " + + (lastError != null ? lastError.getMessage() : "none"), + lastError); + } + + // Never persist SparkFiles paths in VAR_PACKAGE_URI (not valid on other JVMs) + String uriToKeep = preservedUri; + if (StringUtils.isEmpty(uriToKeep) || isSparkFilesLocalPath(uriToKeep)) { + uriToKeep = !isSparkFilesLocalPath(usedPath) ? usedPath : preservedUri; + } + // Always overwrite PROJECT_HOME with *this* JVM's extract (not the driver's /tmp path). + applyToVariables(variables, m, uriToKeep); + if (StringUtils.isNotEmpty(sparkFile)) { + variables.setVariable(VAR_PACKAGE_SPARK_FILE, sparkFile); + } + + String home = variables.getVariable("PROJECT_HOME"); + if (StringUtils.isEmpty(home) || !new File(home).isDirectory()) { + throw new HopException( + "Spark project package materialize did not produce a PROJECT_HOME directory on this" + + " host (got '" + + home + + "') from package path " + + usedPath + + ". Package URI=" + + uriToKeep + + "."); + } + } + + /** + * Drop {@code PROJECT_HOME} when it does not exist as a directory on this JVM (typical: driver + * {@code /local_disk0/tmp/hop-spark-pkg-…} broadcast to executors). + */ + static void clearStaleProjectHome(IVariables variables) { + if (variables == null) { + return; + } + String projectHome = variables.getVariable("PROJECT_HOME"); + if (StringUtils.isEmpty(projectHome)) { + return; + } + if (!new File(projectHome).isDirectory()) { + variables.setVariable("PROJECT_HOME", null); + } + } + + /** + * Resolve the preferred zip path to open on this JVM (first candidate from {@link + * #listPackagePathsForMaterialize}). + */ + public static String resolvePackagePathForMaterialize(IVariables variables) { + java.util.List paths = listPackagePathsForMaterialize(variables); + return paths.isEmpty() ? null : paths.get(0); + } + + /** + * Ordered candidate package zip paths for this JVM. + * + *

    + *
  1. {@code SparkFiles.get(basename)} when the file exists (reliable after driver {@code + * addFile} of a local copy) + *
  2. {@code $HOP_DATA/packages/} (shared data plane) + *
  3. Cluster-shared {@link #VAR_PACKAGE_URI} (UC {@code /Volumes/…}, {@code /dbfs/}, object + * store) — returned even when {@link File#isFile()} is false (FUSE lag / executor mount + * quirks); open is attempted via Hop VFS in {@link #materialize} + *
  4. {@link #VAR_PACKAGE_URI} when that path exists locally, or is a remote {@code scheme://} + * URI + *
+ */ + public static java.util.List listPackagePathsForMaterialize(IVariables variables) { + java.util.LinkedHashSet ordered = new java.util.LinkedHashSet<>(); + if (variables == null) { + return java.util.List.of(); + } + String rawUri = variables.getVariable(VAR_PACKAGE_URI); + String resolvedUri = StringUtils.isNotEmpty(rawUri) ? variables.resolve(rawUri) : null; + + String basename = variables.getVariable(VAR_PACKAGE_SPARK_FILE); + if (StringUtils.isEmpty(basename) && StringUtils.isNotEmpty(resolvedUri)) { + File f = resolveLocalPackageFile(resolvedUri); + if (f != null) { + basename = f.getName(); + } else { + int slash = Math.max(resolvedUri.lastIndexOf('/'), resolvedUri.lastIndexOf('\\')); + if (slash >= 0 && slash < resolvedUri.length() - 1) { + basename = resolvedUri.substring(slash + 1); + } + } + } + + // 1) SparkFiles first — local on every executor after addFile(localCopy) + if (StringUtils.isNotEmpty(basename)) { + try { + String downloaded = org.apache.spark.SparkFiles.get(basename.trim()); + if (StringUtils.isNotEmpty(downloaded) && resolveLocalPackageFile(downloaded) != null) { + ordered.add(downloaded); + } + } catch (Throwable t) { + // not distributed or not ready + } + } + + // 2) Shared data plane: $HOP_DATA/packages/ + if (StringUtils.isNotEmpty(basename)) { + String hopData = variables.getVariable("HOP_DATA"); + if (StringUtils.isNotEmpty(hopData)) { + String dataRoot = variables.resolve(hopData).replace("file://", ""); + while (dataRoot.endsWith("/") || dataRoot.endsWith("\\")) { + dataRoot = dataRoot.substring(0, dataRoot.length() - 1); + } + File shared = new File(dataRoot + File.separator + "packages", basename.trim()); + if (shared.isFile()) { + ordered.add(shared.getAbsolutePath()); + } + } + } + + // 3) Cluster-shared URI (Volumes / DBFS / object store) — try even if File.isFile is false + if (StringUtils.isNotEmpty(resolvedUri) && isClusterSharedPackagePath(resolvedUri)) { + ordered.add(resolvedUri); + } + + // 4) Explicit package URI (local file or remote VFS) + if (StringUtils.isNotEmpty(resolvedUri) && !ordered.contains(resolvedUri)) { + if (resolveLocalPackageFile(resolvedUri) != null) { + ordered.add(resolvedUri); + } else if (resolvedUri.contains("://") && !resolvedUri.startsWith("file:")) { + ordered.add(resolvedUri); + } + } + return new java.util.ArrayList<>(ordered); + } + + /** True for Spark's ephemeral per-JVM download dirs (not safe to ship in variables). */ + static boolean isSparkFilesLocalPath(String path) { + if (StringUtils.isEmpty(path)) { + return false; + } + // SparkFiles root looks like …/userFiles-/basename — never treat as portable URI + return path.replace('\\', '/').contains("/userFiles-"); + } + + /** + * True when every cluster node can open the same package path without SparkFiles (Databricks UC + * Volumes, classic DBFS mount, or remote object-store URI). + */ + static boolean isClusterSharedPackagePath(String path) { + if (StringUtils.isEmpty(path)) { + return false; + } + String p = path.trim().replace('\\', '/'); + if (p.startsWith("/Volumes/") || p.startsWith("/dbfs/")) { + return true; + } + if (p.startsWith("dbfs:")) { + return true; + } + // Object store / remote VFS (not local file:) + int scheme = p.indexOf("://"); + return scheme > 0 && !p.startsWith("file:"); + } + + /** + * Make the project package available on executors. + * + *

Always stages a local filesystem copy and registers it with {@code + * SparkContext.addFile} when possible, setting {@link #VAR_PACKAGE_SPARK_FILE}. Do not + * call {@code addFile} on a UC Volume path directly — Databricks often fails with {@code Stream + * '/files/…' was not found}; copy to {@code java.io.tmpdir} first via {@link + * #ensureLocalPackageFile}. + * + *

When {@link #VAR_PACKAGE_URI} is already on a cluster-shared path (UC + * {@code /Volumes/…}, {@code /dbfs/}, {@code s3a://…}, …), that portable URI is kept so workers + * can also open the shared path; the SparkFiles copy is the reliable fallback when Volume FUSE is + * not visible the same way on every executor JVM. + */ + public static void distributeToCluster( + org.apache.spark.sql.SparkSession spark, IVariables variables) throws HopException { + if (spark == null || variables == null) { + return; + } + String raw = variables.getVariable(VAR_PACKAGE_URI); + if (StringUtils.isEmpty(raw)) { + return; + } + String uri = variables.resolve(raw); + if (StringUtils.isEmpty(uri)) { + return; + } + + boolean clusterShared = isClusterSharedPackagePath(uri); + if (clusterShared) { + // Keep the portable shared URI for workers that can open it directly. + variables.setVariable(VAR_PACKAGE_URI, uri); + } + + // Stage a *true* local filesystem copy for addFile. Never pass /Volumes or dbfs paths to + // addFile — Databricks often fails with "Stream '/files/…' was not found". + String localPath; + try { + localPath = ensureLocalPackageFile(uri); + } catch (HopException e) { + if (clusterShared) { + System.err.println( + ">>>>>> WARNING: could not stage Spark project package for SparkFiles (" + + e.getMessage() + + "); workers will try cluster-shared URI only: " + + uri); + return; + } + throw e; + } + if (isClusterSharedPackagePath(localPath) || isSparkFilesLocalPath(localPath)) { + // ensureLocalPackageFile must not return a Volume path (addFile would fail). + throw new HopException( + "Staged package path is not a plain local file (still looks cluster-shared or" + + " SparkFiles): " + + localPath + + " (source " + + uri + + ")"); + } + String basename = new File(localPath).getName(); + if (StringUtils.isEmpty(basename)) { + throw new HopException("Spark project package has empty filename: " + localPath); + } + + String appId; + try { + appId = spark.sparkContext().applicationId(); + } catch (Exception e) { + appId = "unknown"; + } + String distKey = appId + "|" + localPath; + boolean addFileOk = DISTRIBUTED.contains(distKey); + if (DISTRIBUTED.add(distKey)) { + try { + spark.sparkContext().addFile(localPath); + addFileOk = true; + } catch (Exception e) { + DISTRIBUTED.remove(distKey); + addFileOk = false; + if (!clusterShared) { + throw new HopException( + "Failed to distribute Spark project package via SparkContext.addFile: " + localPath, + e); + } + System.err.println( + ">>>>>> WARNING: SparkContext.addFile failed for staged package " + + localPath + + " (" + + e.getMessage() + + "); workers will try cluster-shared URI only: " + + uri); + } + } + + // Non-shared: rewrite URI to the local staged path used for distribution. + if (!clusterShared && !isSparkFilesLocalPath(localPath)) { + variables.setVariable(VAR_PACKAGE_URI, localPath); + } + // Only advertise SparkFiles basename when addFile succeeded (or was already registered). + if (addFileOk) { + variables.setVariable(VAR_PACKAGE_SPARK_FILE, basename); + } + } + + /** + * Ensure {@code packageUri} is a plain local filesystem path suitable for {@code + * SparkContext.addFile}. + * + *

UC Volumes / DBFS / object-store URIs are always copied into {@code + * java.io.tmpdir}. Returning {@code /Volumes/…} directly looks like a local file on the driver + * ({@link File#isFile()} is true) but {@code addFile} of that path fails on Databricks executors. + */ + public static String ensureLocalPackageFile(String packageUri) throws HopException { + if (StringUtils.isEmpty(packageUri)) { + throw new HopException("Spark project package URI is empty"); + } + String uri = packageUri.trim(); + + // Always stage cluster-shared paths to a real local temp zip for addFile. + if (isClusterSharedPackagePath(uri)) { + return stagePackageToLocalTemp(uri); + } + + File asFile = new File(uri); + if (asFile.isFile()) { + return asFile.getAbsolutePath(); + } + // file:/ URI without Hop VFS + if (uri.startsWith("file:")) { + try { + File f = new File(java.net.URI.create(uri)); + if (f.isFile()) { + return f.getAbsolutePath(); + } + } catch (Exception ignored) { + // fall through to HopVfs + } + } + return stagePackageToLocalTemp(uri); + } + + /** + * Copy {@code packageUri} into {@code java.io.tmpdir}/hop-spark-pkg-src-<key>.zip when + * missing or stale (size / mtime vs local source when visible). + */ + static String stagePackageToLocalTemp(String packageUri) throws HopException { + if (StringUtils.isEmpty(packageUri)) { + throw new HopException("Spark project package URI is empty"); + } + File local = + new File( + System.getProperty("java.io.tmpdir"), + "hop-spark-pkg-src-" + cacheKey(packageUri) + ".zip"); + boolean needCopy = !local.isFile() || local.length() == 0; + File sourceFile = resolveLocalPackageFile(packageUri); + if (!needCopy && sourceFile != null) { + if (sourceFile.length() != local.length() + || sourceFile.lastModified() > local.lastModified()) { + needCopy = true; + } + } + if (needCopy) { + try { + if (local.exists() && !local.delete() && local.isFile()) { + // overwrite via FileOutputStream truncate below + } + try (InputStream in = openPackageStream(packageUri); + OutputStream out = Files.newOutputStream(local.toPath())) { + in.transferTo(out); + } + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException( + "Unable to stage Spark project package locally for distribution: " + packageUri, e); + } + } + if (!local.isFile() || local.length() == 0) { + throw new HopException( + "Staged Spark project package is empty or missing after copy: " + + local.getAbsolutePath() + + " (source " + + packageUri + + ")"); + } + return local.getAbsolutePath(); + } + + /** Set package URI + PROJECT_HOME (+ optional metadata path helper is caller-side). */ + public static void applyToVariables(IVariables variables, Materialized m, String packageUri) { + if (variables == null || m == null) { + return; + } + if (StringUtils.isNotEmpty(packageUri)) { + variables.setVariable(VAR_PACKAGE_URI, packageUri); + } + variables.setVariable("PROJECT_HOME", m.projectHome()); + } + + /** + * Extract {@code packageUri} under the JVM temp directory if not already done for the current + * package fingerprint (size + mtime + sha-256). + * + * @param packageUri path or VFS URI to the package zip + */ + public static Materialized materialize(String packageUri) throws HopException { + if (StringUtils.isEmpty(packageUri)) { + throw new HopException("Spark project package URI is empty"); + } + String key = cacheKey(packageUri); + String fingerprint; + try { + fingerprint = fingerprintPackage(packageUri); + } catch (Exception e) { + throw new HopException("Unable to fingerprint Spark project package: " + packageUri, e); + } + + MaterializedState state = MATERIALIZED.get(key); + if (state != null + && fingerprint.equals(state.fingerprint()) + && isUsableExtractRoot(state.extractRoot())) { + return buildMaterialized(state.extractRoot(), packageUri); + } + synchronized (MATERIALIZED) { + state = MATERIALIZED.get(key); + if (state != null + && fingerprint.equals(state.fingerprint()) + && isUsableExtractRoot(state.extractRoot())) { + return buildMaterialized(state.extractRoot(), packageUri); + } + try { + File extractRoot = new File(System.getProperty("java.io.tmpdir"), "hop-spark-pkg-" + key); + File complete = new File(extractRoot, COMPLETE_MARKER); + String stored = complete.isFile() ? Files.readString(complete.toPath()).trim() : null; + boolean fingerprintMatches = + fingerprint.equals(stored) || isLegacyCompleteMarker(stored, packageUri, fingerprint); + + if (!fingerprintMatches || !isUsableExtractRoot(extractRoot.getAbsolutePath())) { + if (extractRoot.exists()) { + deleteRecursively(extractRoot); + } + if (!extractRoot.mkdirs() && !extractRoot.isDirectory()) { + throw new HopException("Unable to create package extract dir: " + extractRoot); + } + extractZip(packageUri, extractRoot); + Files.writeString(complete.toPath(), fingerprint, StandardCharsets.UTF_8); + } + MATERIALIZED.put(key, new MaterializedState(extractRoot.getAbsolutePath(), fingerprint)); + return buildMaterialized(extractRoot.getAbsolutePath(), packageUri); + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException("Error materializing Spark project package: " + packageUri, e); + } + } + } + + /** True when extract root exists and {@link #detectProjectHome} yields a directory. */ + static boolean isUsableExtractRoot(String extractRootPath) { + if (StringUtils.isEmpty(extractRootPath)) { + return false; + } + File extractRoot = new File(extractRootPath); + if (!extractRoot.isDirectory()) { + return false; + } + try { + return detectProjectHome(extractRoot).isDirectory(); + } catch (HopException e) { + return false; + } + } + + /** + * Older markers stored only the package URI. Accept them only when size/mtime still match the + * current zip so first run after upgrade does not always re-extract unnecessarily when content is + * unchanged; if fingerprint fields cannot be reconciled, force re-extract by returning false. + */ + static boolean isLegacyCompleteMarker(String stored, String packageUri, String fingerprint) { + if (StringUtils.isEmpty(stored) || stored.contains("sha256=")) { + return false; + } + // Legacy: marker was just the URI string + if (!stored.equals(packageUri.trim()) && !stored.equals(packageUri)) { + return false; + } + // URI-only markers cannot prove content identity — always re-extract for safety + return false; + } + + /** + * Fingerprint a package zip for cache invalidation: size, last-modified (local files), and + * SHA-256 of contents. + */ + static String fingerprintPackage(String packageUri) throws Exception { + File local = resolveLocalPackageFile(packageUri); + long size = -1L; + long mtime = -1L; + if (local != null) { + size = local.length(); + mtime = local.lastModified(); + } else { + try { + FileObject fo = HopVfs.getFileObject(packageUri); + if (fo.exists() && fo.getType() == FileType.FILE) { + size = fo.getContent().getSize(); + mtime = fo.getContent().getLastModifiedTime(); + } + } catch (Exception ignored) { + // size/mtime optional for remote; sha still required + } + } + + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + try (InputStream raw = openPackageStream(packageUri); + DigestInputStream din = new DigestInputStream(new BufferedInputStream(raw), digest)) { + byte[] buf = new byte[8192]; + while (din.read(buf) >= 0) { + // drain + } + } + String sha = HexFormat.of().formatHex(digest.digest()); + return "uri=" + + packageUri.trim() + + "\nsize=" + + size + + "\nmtime=" + + mtime + + "\nsha256=" + + sha + + "\n"; + } + + private static Materialized buildMaterialized(String extractRootPath, String packageUri) + throws HopException { + File extractRoot = new File(extractRootPath); + File projectHome = detectProjectHome(extractRoot); + String metadata = findMetadataPath(extractRoot, projectHome); + return new Materialized(projectHome.getAbsolutePath(), metadata, extractRootPath); + } + + /** + * Export a Hop project home directory into a Spark-oriented package zip. + * + *

Loads {@link PackageExportFilter#CONFIG_FILENAME} from the project home when present and + * merges it with built-in default exclude dirs. + * + * @param projectHome local/VFS path of the project home (definition tree) + * @param zipFilename destination zip (created/overwritten) + * @param metadataProvider metadata to embed as root {@code metadata.json} (required for + * MainSpark) + */ + public static void exportProject( + String projectHome, + String zipFilename, + IHopMetadataProvider metadataProvider, + IVariables variables) + throws HopException { + exportProject(projectHome, zipFilename, metadataProvider, variables, null); + } + + /** + * Export with an explicit filter layer (CLI/GUI). Merge order: built-in defaults (via empty + * filter) → {@code spark-package.json} in project home → {@code explicitFilter}. + */ + public static void exportProject( + String projectHome, + String zipFilename, + IHopMetadataProvider metadataProvider, + IVariables variables, + PackageExportFilter explicitFilter) + throws HopException { + if (StringUtils.isEmpty(projectHome)) { + throw new HopException("Project home is required for Spark project package export"); + } + if (StringUtils.isEmpty(zipFilename)) { + throw new HopException("Zip filename is required for Spark project package export"); + } + if (metadataProvider == null) { + throw new HopException("Metadata provider is required for Spark project package export"); + } + String realHome = variables != null ? variables.resolve(projectHome) : projectHome; + String realZip = variables != null ? variables.resolve(zipFilename) : zipFilename; + + PackageExportFilter filter = + PackageExportFilter.empty() + .merge(PackageExportFilter.loadFromProjectHome(realHome)) + .merge(explicitFilter); + + try { + FileObject home = HopVfs.getFileObject(realHome); + if (!home.exists() || home.getType() != FileType.FOLDER) { + throw new HopException("Project home is not a folder: " + realHome); + } + + FileObject zipFile = HopVfs.getFileObject(realZip); + if (zipFile.exists()) { + zipFile.delete(); + } + + try (OutputStream os = HopVfs.getOutputStream(zipFile, false); + ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(os))) { + + // Marker + Properties props = new Properties(); + props.setProperty(PROP_PROJECT_ROOT, DEFAULT_PROJECT_ROOT); + props.setProperty("format", "hop-spark-project-package"); + props.setProperty("formatVersion", "1"); + zos.putNextEntry(new ZipEntry(MARKER_ENTRY)); + props.store(zos, "Apache Hop Native Spark project package"); + zos.closeEntry(); + + // Metadata at zip root + String metadataJson = new SerializableMetadataProvider(metadataProvider).toJson(); + zos.putNextEntry(new ZipEntry(METADATA_ENTRY)); + zos.write(metadataJson.getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + + // Project tree under project/ + Set written = new HashSet<>(); + home.findFiles( + new FileSelector() { + @Override + public boolean includeFile(FileSelectInfo fileInfo) throws Exception { + FileObject fo = fileInfo.getFile(); + if (fo.getType() != FileType.FILE) { + return false; + } + String rel = relativePath(home, fo); + if (rel == null || filter.shouldSkipRelative(rel)) { + return false; + } + // Do not nest the destination zip into itself + if (zipFile.equals(fo)) { + return false; + } + String entryName = DEFAULT_PROJECT_ROOT + "/" + rel.replace('\\', '/'); + if (!written.add(entryName)) { + return false; + } + zos.putNextEntry(new ZipEntry(entryName)); + try (InputStream in = HopVfs.getInputStream(fo)) { + in.transferTo(zos); + } + zos.closeEntry(); + return false; + } + + @Override + public boolean traverseDescendents(FileSelectInfo fileInfo) throws Exception { + FileObject fo = fileInfo.getFile(); + if (fo.getType() != FileType.FOLDER) { + return true; + } + String base = fo.getName().getBaseName(); + return !filter.shouldSkipDirectoryBasename(base, fileInfo.getDepth()); + } + }); + } + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException( + "Error exporting Spark project package from '" + realHome + "' to '" + realZip + "'", e); + } + } + + /** + * Resolve a pipeline path against a materialized project home when the path is relative or uses + * {@code ${PROJECT_HOME}}. + */ + public static String resolvePipelinePath( + String pipelinePath, Materialized m, IVariables variables) throws HopException { + if (StringUtils.isEmpty(pipelinePath)) { + throw new HopException("Pipeline path is empty"); + } + String p = variables != null ? variables.resolve(pipelinePath) : pipelinePath; + p = p.trim(); + if (p.contains("://") || p.startsWith("/") || p.matches("^[A-Za-z]:[\\\\/].*")) { + return p; + } + // Strip leading PROJECT_HOME/ if still present after resolve failed to expand + if (p.startsWith("${PROJECT_HOME}/")) { + p = p.substring("${PROJECT_HOME}/".length()); + } + String home = m.projectHome(); + if (!home.endsWith("/") && !home.endsWith("\\")) { + home = home + File.separator; + } + return new File(home, p).getAbsolutePath(); + } + + /** Warning text when a Spark Dataset path still references PROJECT_HOME. */ + public static String projectHomeDataPathWarning(String rawPath) { + if (rawPath == null) { + return null; + } + if (!rawPath.contains("PROJECT_HOME") && !rawPath.contains("${PROJECT_HOME}")) { + return null; + } + return "Spark File/Lake path references PROJECT_HOME ('" + + rawPath + + "'). With a Spark project package, PROJECT_HOME is the extracted *definition* tree " + + "(not object-store data). Prefer HOP_DATA / OUTPUT_ROOT or an explicit Spark URI " + + "(e.g. s3a://…) for Dataset I/O."; + } + + // --- internals --- + + private static String cacheKey(String packageUri) { + // Stable short key; include length-ish hash of URI string + int h = packageUri.trim().hashCode(); + return Integer.toHexString(h); + } + + private static void extractZip(String packageUri, File extractRoot) throws Exception { + // Prefer java.io for local paths (incl. SparkFiles.get downloads). HopVfs often fails to + // resolve bare absolute paths like /tmp/spark-…/userFiles-…/pkg.zip without a file: scheme. + try (InputStream raw = openPackageStream(packageUri); + ZipInputStream zis = new ZipInputStream(new BufferedInputStream(raw))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (entry.isDirectory()) { + continue; + } + String name = entry.getName(); + if (name.contains("..")) { + throw new HopException("Refusing zip entry with '..': " + name); + } + File out = new File(extractRoot, name); + File parent = out.getParentFile(); + if (parent != null && !parent.exists() && !parent.mkdirs() && !parent.isDirectory()) { + throw new HopException("Unable to create directory: " + parent); + } + try (OutputStream os = Files.newOutputStream(out.toPath())) { + zis.transferTo(os); + } + zis.closeEntry(); + } + } + } + + /** + * Open the package zip for reading. Local filesystem first (SparkFiles / prepare-dist paths), + * then Hop VFS for remote URIs. + */ + static InputStream openPackageStream(String packageUri) throws Exception { + if (StringUtils.isEmpty(packageUri)) { + throw new HopException("Spark project package URI is empty"); + } + String uri = packageUri.trim(); + + File local = resolveLocalPackageFile(uri); + if (local != null) { + return new FileInputStream(local); + } + + try { + FileObject zipFo = HopVfs.getFileObject(uri); + if (zipFo.exists()) { + return HopVfs.getInputStream(zipFo); + } + } catch (Exception e) { + throw new HopException( + "Spark project package not found: " + + uri + + ". On workers prefer a shared path ($HOP_DATA/packages/) or SparkFiles " + + "distribution (spark-submit --files / SparkContext.addFile). Local check failed; " + + "HopVfs error: " + + e.getMessage(), + e); + } + throw new HopException( + "Spark project package not found: " + + uri + + ". On workers copy the zip to $HOP_DATA/packages/ (shared volume) and/or pass " + + "spark-submit --files. SparkFiles paths under /tmp/spark-*/userFiles-* are " + + "per-JVM and must not be shared via variables."); + } + + /** + * If {@code uri} denotes an existing local file (plain path or {@code file:} URI), return it; + * otherwise {@code null}. + */ + static File resolveLocalPackageFile(String uri) { + if (StringUtils.isEmpty(uri)) { + return null; + } + try { + if (uri.startsWith("file:")) { + Path p = Path.of(java.net.URI.create(uri)); + File f = p.toFile(); + return f.isFile() ? f : null; + } + } catch (Exception ignored) { + // fall through to plain path + } + File plain = new File(uri); + if (plain.isFile()) { + return plain; + } + // SparkFiles sometimes returns a path that is not yet a regular file but is readable + if (plain.exists() && plain.canRead() && plain.length() > 0) { + return plain; + } + return null; + } + + static File detectProjectHome(File extractRoot) throws HopException { + File marker = new File(extractRoot, MARKER_ENTRY); + if (marker.isFile()) { + try { + Properties props = new Properties(); + try (InputStream in = Files.newInputStream(marker.toPath())) { + props.load(in); + } + String root = props.getProperty(PROP_PROJECT_ROOT, DEFAULT_PROJECT_ROOT); + File project = new File(extractRoot, root); + if (project.isDirectory()) { + return project; + } + } catch (IOException e) { + throw new HopException("Unable to read " + MARKER_ENTRY, e); + } + } + File projectDir = new File(extractRoot, DEFAULT_PROJECT_ROOT); + if (projectDir.isDirectory()) { + return projectDir; + } + // GUI export style: single top-level folder + File[] children = extractRoot.listFiles(f -> !f.getName().startsWith(".")); + if (children != null && children.length == 1 && children[0].isDirectory()) { + return children[0]; + } + return extractRoot; + } + + private static String findMetadataPath(File extractRoot, File projectHome) { + File atRoot = new File(extractRoot, METADATA_ENTRY); + if (atRoot.isFile()) { + return atRoot.getAbsolutePath(); + } + File inProject = new File(projectHome, METADATA_ENTRY); + if (inProject.isFile()) { + return inProject.getAbsolutePath(); + } + return null; + } + + private static String relativePath(FileObject home, FileObject file) throws Exception { + String homeUri = home.getName().getURI(); + String fileUri = file.getName().getURI(); + if (!fileUri.startsWith(homeUri)) { + return null; + } + String rel = fileUri.substring(homeUri.length()); + while (rel.startsWith("/")) { + rel = rel.substring(1); + } + return rel; + } + + private static void deleteRecursively(File f) throws IOException { + if (f == null || !f.exists()) { + return; + } + if (f.isDirectory()) { + File[] kids = f.listFiles(); + if (kids != null) { + for (File k : kids) { + deleteRecursively(k); + } + } + } + Files.deleteIfExists(f.toPath()); + } + + /** Clear JVM caches (tests). */ + public static void clearMaterializationCache() { + MATERIALIZED.clear(); + DISTRIBUTED.clear(); + } + + /** Visible for tests — current in-JVM materialize fingerprint for a package URI, or null. */ + static String cachedFingerprintForTest(String packageUri) { + MaterializedState state = MATERIALIZED.get(cacheKey(packageUri)); + return state == null ? null : state.fingerprint(); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/run/MainSpark.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/run/MainSpark.java new file mode 100644 index 00000000000..4c0978e2c7c --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/run/MainSpark.java @@ -0,0 +1,333 @@ +/* + * 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.hop.spark.run; + +import java.io.IOException; +import java.io.InputStream; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.Const; +import org.apache.hop.core.HopEnvironment; +import org.apache.hop.core.config.DescribedVariablesConfigFile; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.metadata.SerializableMetadataProvider; +import org.apache.hop.core.plugins.IPlugin; +import org.apache.hop.core.plugins.PluginRegistry; +import org.apache.hop.core.variables.DescribedVariable; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.core.vfs.HopVfs; +import org.apache.hop.core.xml.XmlHandler; +import org.apache.hop.metadata.api.IHopMetadataSerializer; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.config.IPipelineEngineRunConfiguration; +import org.apache.hop.pipeline.config.PipelineRunConfiguration; +import org.apache.hop.pipeline.engine.IPipelineEngine; +import org.apache.hop.pipeline.engine.PipelineEngineFactory; +import org.apache.hop.pipeline.engine.PipelineEnginePluginType; +import org.apache.hop.spark.engines.ISparkPipelineEngineRunConfiguration; +import org.apache.hop.spark.pkg.SparkProjectPackage; +import org.apache.hop.spark.util.SparkConst; + +/** + * Driver entry point for native Spark pipeline execution via {@code spark-submit}, analogous to + * Beam's {@code org.apache.hop.beam.run.MainBeam}. + * + *

Typical usage: + * + *

+ * spark-submit --master spark://host:7077 \
+ *   --class org.apache.hop.spark.run.MainSpark \
+ *   hop-native-spark4.jar \
+ *   pipeline.hpl metadata.json runConfigName
+ * 
+ * + *

Project package mode (nested Simple Mapping / Pipeline Executor definitions): + * + *

+ * spark-submit ... MainSpark \
+ *   --HopProjectPackage=/path/project-spark.zip \
+ *   --HopPipelinePath=pipelines/run-on-spark.hpl \
+ *   --HopRunConfigurationName=spark-cluster
+ * 
+ */ +public class MainSpark { + + public static void main(String[] args) { + try { + runPipeline(args); + // Success. Never call System.exit(0) when Databricks TrapExitSecurityManager is installed: + // it throws ExitSecurityException ("Program attempted to exit with code 0"), which older + // MainSpark caught as a failure and re-printed as "Error running native Spark pipeline". + // Plain spark-submit still uses System.exit(0) so non-daemon Spark threads do not hang. + finishSuccess(); + } catch (SecurityException e) { + if (isTrappedExitZero(e)) { + System.out.println( + ">>>>>> System.exit(0) blocked by security manager — treating run as successful"); + return; + } + failAndExit(e); + } catch (Exception e) { + failAndExit(e); + } + } + + private static void runPipeline(String[] args) throws Exception { + System.out.println(">>>>>> Initializing Hop"); + System.out.println( + ">>>>>> Spark project package distribution build: " + + SparkProjectPackage.DISTRIBUTION_BUILD_ID); + try { + java.net.URL loc = + SparkProjectPackage.class.getProtectionDomain().getCodeSource().getLocation(); + if (loc != null) { + System.out.println(">>>>>> SparkProjectPackage loaded from: " + loc); + } + } catch (Exception ignored) { + // best-effort diagnostics only + } + HopEnvironment.init(); + + MainSparkArgs parsed = MainSparkArgs.parse(args); + System.out.println( + "Argument : Pipeline path (.hpl) : " + parsed.getPipelinePath()); + if (parsed.hasProjectPackage()) { + System.out.println( + "Argument : Project package (zip) : " + parsed.getProjectPackage()); + } + System.out.println( + "Argument : Metadata export (.json) : " + + Const.NVL(parsed.getMetadataPath(), "(from package)")); + System.out.println( + "Argument : Pipeline run configuration : " + parsed.getRunConfigName()); + if (StringUtils.isNotEmpty(parsed.getEnvironmentFile())) { + System.out.println( + "Argument : Environment configuration file : " + parsed.getEnvironmentFile()); + } + + IVariables variables = Variables.getADefaultVariableSpace(); + + if (StringUtils.isNotEmpty(parsed.getEnvironmentFile())) { + DescribedVariablesConfigFile configFile = + new DescribedVariablesConfigFile(variables.resolve(parsed.getEnvironmentFile())); + configFile.readFromFile(); + for (DescribedVariable variable : configFile.getDescribedVariables()) { + variables.setVariable(variable.getName(), variable.getValue()); + } + System.out.println( + ">>>>>> Applied number of variables: " + configFile.getDescribedVariables().size()); + } + + String pipelinePath = parsed.getPipelinePath(); + String metadataPath = parsed.getMetadataPath(); + + if (parsed.hasProjectPackage()) { + String packageUri = variables.resolve(parsed.getProjectPackage()); + System.out.println(">>>>>> Materializing Spark project package: " + packageUri); + SparkProjectPackage.Materialized materialized = SparkProjectPackage.materialize(packageUri); + SparkProjectPackage.applyToVariables(variables, materialized, packageUri); + System.out.println(">>>>>> PROJECT_HOME=" + materialized.projectHome()); + System.out.println( + ">>>>>> Note: PROJECT_HOME is the definition package root — use separate variables" + + " (HOP_DATA / s3a://…) for Spark Dataset data paths."); + pipelinePath = SparkProjectPackage.resolvePipelinePath(pipelinePath, materialized, variables); + if (StringUtils.isEmpty(metadataPath)) { + if (StringUtils.isEmpty(materialized.metadataPath())) { + throw new HopException( + "Project package has no metadata.json and --HopMetadataPath was not set: " + + packageUri); + } + metadataPath = materialized.metadataPath(); + } else { + metadataPath = variables.resolve(metadataPath); + } + System.out.println(">>>>>> Resolved pipeline path: " + pipelinePath); + System.out.println(">>>>>> Resolved metadata path: " + metadataPath); + } else { + pipelinePath = variables.resolve(pipelinePath); + metadataPath = variables.resolve(metadataPath); + } + + String pipelineMetaXml = readFileIntoString(pipelinePath, Const.UTF_8); + String metadataJson = readFileIntoString(metadataPath, Const.UTF_8); + String runConfigName = parsed.getRunConfigName(); + + SerializableMetadataProvider metadataProvider = new SerializableMetadataProvider(metadataJson); + + IHopMetadataSerializer serializer = + metadataProvider.getSerializer(PipelineRunConfiguration.class); + if (!serializer.exists(runConfigName)) { + throw new HopException( + "The specified pipeline run configuration '" + + runConfigName + + "' doesn't exist in the metadata export"); + } + + PipelineRunConfiguration pipelineRunConfiguration = serializer.load(runConfigName); + IPipelineEngineRunConfiguration engineRunConfiguration = + pipelineRunConfiguration.getEngineRunConfiguration(); + if (!(engineRunConfiguration instanceof ISparkPipelineEngineRunConfiguration)) { + throw new HopException( + "Run configuration '" + + runConfigName + + "' is not a native Spark pipeline engine configuration (found " + + (engineRunConfiguration == null + ? "null" + : engineRunConfiguration.getClass().getName()) + + "). Use a run configuration whose engine is '" + + SparkConst.PLUGIN_NAME + + "'."); + } + + System.out.println(">>>>>> Loading pipeline metadata"); + PipelineMeta pipelineMeta = + new PipelineMeta( + XmlHandler.loadXmlString(pipelineMetaXml, PipelineMeta.XML_TAG), metadataProvider); + // Match hop-run: when name is synchronized with the filename, use the provided path so + // getName() returns the basename (e.g. spark-transforms) instead of the XML . + pipelineMeta.setFilename(pipelinePath); + + System.out.println(">>>>>> Validating native Spark engine plugin in fat jar..."); + PluginRegistry registry = PluginRegistry.getInstance(); + IPlugin sparkEnginePlugin = + registry.findPluginWithId(PipelineEnginePluginType.class, SparkConst.PLUGIN_ID); + if (sparkEnginePlugin == null) { + throw new HopException( + "ERROR: Unable to find native Spark pipeline engine plugin '" + + SparkConst.PLUGIN_ID + + "'. Is plugins/engines/spark included in the fat jar? " + + "Generate with: hop-conf.sh --generate-fat-jar=... --spark-client-version=native"); + } + + // Diagnose SPARK_HOME mismatches: if SPARK_HOME points at an older install + // (e.g. /opt/spark -> 3.3.0), even "./bin/spark-submit" from a 4.1.x tree re-executes + // ${SPARK_HOME}/bin/spark-class and loads that older Spark. + String sparkHome = System.getenv("SPARK_HOME"); + if (StringUtils.isNotEmpty(sparkHome)) { + System.out.println(">>>>>> SPARK_HOME=" + sparkHome); + } + + IPipelineEngine pipelineEngine = + PipelineEngineFactory.createPipelineEngine( + variables, runConfigName, metadataProvider, pipelineMeta); + System.out.println(">>>>>> Pipeline execution starting (native Spark)..."); + pipelineEngine.execute(); + pipelineEngine.waitUntilFinished(); + System.out.println(">>>>>> Execution finished..."); + if (pipelineEngine.getErrors() > 0) { + throw new HopException("Pipeline finished with " + pipelineEngine.getErrors() + " error(s)"); + } + } + + /** + * Complete a successful run without tripping Databricks {@code TrapExitSecurityManager}. + * + *

Databricks installs a security manager that rejects {@code System.exit} (including code 0) + * with {@code ExitSecurityException}. Returning from {@code main} is the supported success path + * there. Classic spark-submit still needs {@code System.exit(0)} so non-daemon Spark threads do + * not hang the JVM. + */ + static void finishSuccess() { + if (shouldAvoidSystemExitOnSuccess()) { + System.out.println(">>>>>> Skipping System.exit(0) (Databricks / TrapExit security manager)"); + return; + } + try { + System.exit(0); + } catch (SecurityException e) { + if (isTrappedExitZero(e)) { + System.out.println( + ">>>>>> System.exit(0) blocked by security manager — treating run as successful"); + return; + } + throw e; + } + } + + private static void failAndExit(Exception e) { + System.err.println("Error running native Spark pipeline: " + e.getMessage()); + e.printStackTrace(); + if (shouldAvoidSystemExitOnSuccess()) { + // System.exit(1) is also trapped on Databricks; rethrow so the driver reports failure. + if (e instanceof RuntimeException re) { + throw re; + } + throw new RuntimeException("Error running native Spark pipeline: " + e.getMessage(), e); + } + try { + System.exit(1); + } catch (SecurityException se) { + throw new RuntimeException("Error running native Spark pipeline: " + e.getMessage(), e); + } + } + + /** + * True when running under Databricks Runtime or any JVM that installs a TrapExit-style security + * manager (detected without Spark API imports at class load). + */ + static boolean isDatabricksEnvironment() { + if (StringUtils.isNotEmpty(System.getenv("DATABRICKS_RUNTIME_VERSION"))) { + return true; + } + String sparkHome = System.getenv("SPARK_HOME"); + if (StringUtils.isNotEmpty(sparkHome) && sparkHome.contains("/databricks")) { + return true; + } + return hasTrapExitSecurityManager(); + } + + /** Prefer returning from main over System.exit(0) when exit is trapped or forbidden. */ + static boolean shouldAvoidSystemExitOnSuccess() { + return isDatabricksEnvironment(); + } + + static boolean hasTrapExitSecurityManager() { + try { + SecurityManager sm = System.getSecurityManager(); + if (sm == null) { + return false; + } + String name = sm.getClass().getName(); + return name.contains("TrapExit") || name.contains("databricks"); + } catch (Throwable t) { + return false; + } + } + + /** Databricks {@code ExitSecurityException}: "Program attempted to exit with code 0". */ + static boolean isTrappedExitZero(Throwable t) { + if (!(t instanceof SecurityException)) { + return false; + } + String msg = t.getMessage(); + if (msg == null) { + return false; + } + String lower = msg.toLowerCase(); + return lower.contains("exit with code 0") || lower.contains("attempted to exit with code 0"); + } + + private static String readFileIntoString(String filename, String encoding) throws IOException { + try (InputStream inputStream = HopVfs.getInputStream(filename)) { + return IOUtils.toString(inputStream, encoding); + } catch (Exception e) { + throw new IOException("Error reading from file " + filename, e); + } + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/run/MainSparkArgs.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/run/MainSparkArgs.java new file mode 100644 index 00000000000..335986ad3fd --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/run/MainSparkArgs.java @@ -0,0 +1,151 @@ +/* + * 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.hop.spark.run; + +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.exception.HopException; + +/** + * Parsed arguments for {@link MainSpark} (positional MainBeam-style or named {@code --Hop*} form). + */ +public final class MainSparkArgs { + + public static final String USAGE = + "Usage:\n" + + " MainSpark [env-config.json]\n" + + " MainSpark --HopPipelinePath=... --HopMetadataPath=... --HopRunConfigurationName=..." + + " [--HopConfigFile=...]\n" + + " MainSpark --HopProjectPackage=package.zip --HopPipelinePath=relative/or/path.hpl" + + " --HopRunConfigurationName=... [--HopMetadataPath=...] [--HopConfigFile=...]"; + + private final String pipelinePath; + private final String metadataPath; + private final String runConfigName; + private final String environmentFile; + private final String projectPackage; + + public MainSparkArgs( + String pipelinePath, + String metadataPath, + String runConfigName, + String environmentFile, + String projectPackage) { + this.pipelinePath = pipelinePath; + this.metadataPath = metadataPath; + this.runConfigName = runConfigName; + this.environmentFile = environmentFile; + this.projectPackage = projectPackage; + } + + public String getPipelinePath() { + return pipelinePath; + } + + public String getMetadataPath() { + return metadataPath; + } + + public String getRunConfigName() { + return runConfigName; + } + + public String getEnvironmentFile() { + return environmentFile; + } + + /** Optional Spark project package zip URI ({@code --HopProjectPackage}). */ + public String getProjectPackage() { + return projectPackage; + } + + public boolean hasProjectPackage() { + return StringUtils.isNotEmpty(projectPackage); + } + + /** + * Parse CLI arguments. Named form is used when the first argument starts with {@code --}. + * + * @throws HopException if required arguments are missing + */ + public static MainSparkArgs parse(String[] args) throws HopException { + if (args == null || args.length == 0) { + throw new HopException("No arguments provided.\n" + USAGE); + } + + String pipelinePath = null; + String metadataPath = null; + String runConfigName = null; + String environmentFile = null; + String projectPackage = null; + + if (args[0].startsWith("--")) { + for (String arg : args) { + String[] split = arg.split("=", 2); + String key = split.length > 0 ? split[0] : null; + String value = split.length > 1 ? split[1] : null; + if (key == null) { + continue; + } + switch (key) { + case "--HopPipelinePath": + pipelinePath = value; + break; + case "--HopMetadataPath": + metadataPath = value; + break; + case "--HopRunConfigurationName": + runConfigName = value; + break; + case "--HopConfigFile": + environmentFile = value; + break; + case "--HopProjectPackage": + projectPackage = value; + break; + default: + break; + } + } + } else { + if (args.length < 3) { + throw new HopException( + "Expected at least 3 positional arguments: pipeline.hpl metadata.json runConfigName\n" + + USAGE); + } + pipelinePath = args[0]; + metadataPath = args[1]; + runConfigName = args[2]; + if (args.length > 3) { + environmentFile = args[3]; + } + } + + if (StringUtils.isEmpty(pipelinePath) || StringUtils.isEmpty(runConfigName)) { + throw new HopException("Pipeline path and run configuration name are required.\n" + USAGE); + } + if (StringUtils.isEmpty(projectPackage) && StringUtils.isEmpty(metadataPath)) { + throw new HopException( + "Metadata path is required unless --HopProjectPackage is set (package supplies" + + " metadata.json).\n" + + USAGE); + } + + return new MainSparkArgs( + pipelinePath, metadataPath, runConfigName, environmentFile, projectPackage); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/LakeSessionPlan.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/LakeSessionPlan.java new file mode 100644 index 00000000000..712ac39de13 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/LakeSessionPlan.java @@ -0,0 +1,427 @@ +/* + * 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.hop.spark.table; + +import java.nio.file.Paths; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.core.xml.XmlHandler; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.ITransformMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.metadata.SparkCatalog; +import org.apache.hop.spark.pipeline.HopPipelineMetaToSparkConverter; +import org.apache.hop.spark.transforms.table.SparkLakeTableInputMeta; +import org.apache.hop.spark.transforms.table.SparkLakeTableMaintenanceMeta; +import org.apache.hop.spark.transforms.table.SparkLakeTableMergeMeta; +import org.apache.hop.spark.transforms.table.SparkLakeTableOutputMeta; +import org.apache.hop.spark.util.SparkConst; +import org.apache.spark.sql.SparkSession; +import org.w3c.dom.Node; + +/** + * Pre-session scan of active lake transforms: formats, referenced {@link SparkCatalog} metadata, + * and session conf for Delta / Iceberg PATH + TABLE modes. + */ +public final class LakeSessionPlan { + + private static final Set LAKE_PLUGIN_IDS = + Set.of( + SparkConst.SPARK_LAKE_TABLE_INPUT_PLUGIN_ID, + SparkConst.SPARK_LAKE_TABLE_OUTPUT_PLUGIN_ID, + SparkConst.SPARK_LAKE_TABLE_MERGE_PLUGIN_ID, + SparkConst.SPARK_LAKE_TABLE_MAINTENANCE_PLUGIN_ID); + + private final Set formatsNeeded = new LinkedHashSet<>(); + private final Map catalogsByMetaName = new LinkedHashMap<>(); + private boolean needsTableMode; + + private LakeSessionPlan() {} + + public Set getFormatsNeeded() { + return formatsNeeded; + } + + public Map getCatalogsByMetaName() { + return catalogsByMetaName; + } + + public boolean isEmpty() { + return formatsNeeded.isEmpty(); + } + + public boolean needsDelta() { + return formatsNeeded.contains(SparkLakeFormats.FORMAT_DELTA); + } + + public boolean needsIceberg() { + return formatsNeeded.contains(SparkLakeFormats.FORMAT_ICEBERG); + } + + public boolean needsTableMode() { + return needsTableMode; + } + + /** Scan active transforms for lake plugin ids, formats, and SparkCatalog metadata references. */ + public static LakeSessionPlan from( + PipelineMeta pipelineMeta, IHopMetadataProvider metadataProvider) throws HopException { + return from(pipelineMeta, metadataProvider, null); + } + + public static LakeSessionPlan from( + PipelineMeta pipelineMeta, IHopMetadataProvider metadataProvider, IVariables variables) + throws HopException { + LakeSessionPlan plan = new LakeSessionPlan(); + if (pipelineMeta == null) { + return plan; + } + List active = + HopPipelineMetaToSparkConverter.collectActiveTransforms(pipelineMeta); + for (TransformMeta tm : active) { + String id = tm.getTransformPluginId(); + if (id == null || !LAKE_PLUGIN_IDS.contains(id)) { + continue; + } + if (SparkConst.SPARK_LAKE_TABLE_INPUT_PLUGIN_ID.equals(id)) { + SparkLakeTableInputMeta meta = new SparkLakeTableInputMeta(); + loadMeta(meta, tm, metadataProvider); + SparkLakeTableSupport.collectFormat(meta.getFormat(), plan.formatsNeeded); + collectCatalogRef( + plan, + metadataProvider, + variables, + meta.getIdentifierMode(), + meta.getCatalogMetadataName()); + } else if (SparkConst.SPARK_LAKE_TABLE_OUTPUT_PLUGIN_ID.equals(id)) { + SparkLakeTableOutputMeta meta = new SparkLakeTableOutputMeta(); + loadMeta(meta, tm, metadataProvider); + SparkLakeTableSupport.collectFormat(meta.getFormat(), plan.formatsNeeded); + collectCatalogRef( + plan, + metadataProvider, + variables, + meta.getIdentifierMode(), + meta.getCatalogMetadataName()); + } else if (SparkConst.SPARK_LAKE_TABLE_MERGE_PLUGIN_ID.equals(id)) { + SparkLakeTableMergeMeta meta = new SparkLakeTableMergeMeta(); + loadMeta(meta, tm, metadataProvider); + SparkLakeTableSupport.collectFormat(meta.getFormat(), plan.formatsNeeded); + collectCatalogRef( + plan, + metadataProvider, + variables, + meta.getIdentifierMode(), + meta.getCatalogMetadataName()); + } else if (SparkConst.SPARK_LAKE_TABLE_MAINTENANCE_PLUGIN_ID.equals(id)) { + SparkLakeTableMaintenanceMeta meta = new SparkLakeTableMaintenanceMeta(); + loadMeta(meta, tm, metadataProvider); + SparkLakeTableSupport.collectFormat(meta.getFormat(), plan.formatsNeeded); + collectCatalogRef( + plan, + metadataProvider, + variables, + meta.getIdentifierMode(), + meta.getCatalogMetadataName()); + } + } + return plan; + } + + private static void collectCatalogRef( + LakeSessionPlan plan, + IHopMetadataProvider metadataProvider, + IVariables variables, + String identifierMode, + String catalogMetadataName) + throws HopException { + String mode; + try { + mode = SparkLakeTableSupport.normalizeIdentifierMode(identifierMode); + } catch (HopException e) { + mode = SparkLakeTableInputMeta.MODE_PATH; + } + if (SparkLakeTableInputMeta.MODE_TABLE.equals(mode)) { + plan.needsTableMode = true; + } + if (StringUtils.isEmpty(catalogMetadataName) || metadataProvider == null) { + return; + } + String metaName = + variables != null ? variables.resolve(catalogMetadataName) : catalogMetadataName.trim(); + if (StringUtils.isEmpty(metaName) || plan.catalogsByMetaName.containsKey(metaName)) { + return; + } + try { + SparkCatalog catalog = metadataProvider.getSerializer(SparkCatalog.class).load(metaName); + if (catalog == null) { + throw new HopException( + "SparkCatalog metadata '" + + metaName + + "' not found (referenced by a Lake Table transform)"); + } + plan.catalogsByMetaName.put(metaName, catalog); + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException("Unable to load SparkCatalog metadata '" + metaName + "'", e); + } + } + + /** Classpath probe for all formats in this plan. */ + public void verifyClasspath(ClassLoader classLoader) throws HopException { + if (formatsNeeded.isEmpty()) { + return; + } + SparkLakeConnectorProbe.verifyClasspath(formatsNeeded, classLoader); + } + + /** + * Apply Delta / Iceberg session conf and referenced SparkCatalog entries on a new hop-run session + * builder. + */ + public void applyToBuilder(SparkSession.Builder builder) throws HopException { + applyToBuilder(builder, null); + } + + public void applyToBuilder(SparkSession.Builder builder, IVariables variables) + throws HopException { + if (formatsNeeded.isEmpty() && catalogsByMetaName.isEmpty()) { + return; + } + LinkedHashSet extensions = new LinkedHashSet<>(); + if (needsDelta()) { + extensions.add(SparkLakeFormats.DELTA_EXTENSION); + builder.config(SparkLakeFormats.SPARK_CONF_SPARK_CATALOG, SparkLakeFormats.DELTA_CATALOG); + } + if (needsIceberg()) { + extensions.add(SparkLakeFormats.ICEBERG_EXTENSIONS); + // PATH-mode helper catalog (also useful as default for path identifiers) + builder.config( + SparkLakeFormats.SPARK_CONF_ICEBERG_PATH_CATALOG, SparkLakeFormats.ICEBERG_CATALOG); + builder.config(SparkLakeFormats.SPARK_CONF_ICEBERG_PATH_CATALOG_TYPE, "hadoop"); + builder.config( + SparkLakeFormats.SPARK_CONF_ICEBERG_PATH_CATALOG_WAREHOUSE, + defaultIcebergPathWarehouse()); + } + if (!extensions.isEmpty()) { + builder.config(SparkLakeFormats.SPARK_CONF_EXTENSIONS, String.join(",", extensions)); + } + for (SparkCatalog catalog : catalogsByMetaName.values()) { + SparkCatalogApplier.applyToBuilder(builder, catalog, variables); + } + } + + /** + * For an active/reused session (spark-submit): require connector classes and Delta conf; register + * missing Iceberg path catalog and user SparkCatalog entries when possible. + */ + public void verifyActiveSession(SparkSession session, ILogChannel log) throws HopException { + verifyActiveSession(session, log, null); + } + + public void verifyActiveSession(SparkSession session, ILogChannel log, IVariables variables) + throws HopException { + if (formatsNeeded.isEmpty() && catalogsByMetaName.isEmpty()) { + return; + } + ClassLoader cl = LakeSessionPlan.class.getClassLoader(); + if (!formatsNeeded.isEmpty()) { + verifyClasspath(cl); + } + + if (needsDelta()) { + String catalog = session.conf().get(SparkLakeFormats.SPARK_CONF_SPARK_CATALOG, ""); + String extensions = session.conf().get(SparkLakeFormats.SPARK_CONF_EXTENSIONS, ""); + boolean catalogOk = catalog != null && catalog.contains("DeltaCatalog"); + boolean extOk = + extensions != null + && extensions.toLowerCase(Locale.ROOT).contains("deltasparksessionextension"); + if (!catalogOk || !extOk) { + throw new HopException( + "Active SparkSession is missing Delta session configuration required for lake" + + " transforms. Set when launching (spark-submit --conf):\n" + + " " + + SparkLakeFormats.SPARK_CONF_EXTENSIONS + + "=" + + SparkLakeFormats.DELTA_EXTENSION + + "\n " + + SparkLakeFormats.SPARK_CONF_SPARK_CATALOG + + "=" + + SparkLakeFormats.DELTA_CATALOG + + "\nAlso ensure the Delta connector is on the driver/executor classpath" + + " (--packages io.delta:delta-spark_4.1_2.13:4.3.1). Current" + + " spark.sql.extensions='" + + extensions + + "', spark.sql.catalog.spark_catalog='" + + catalog + + "'."); + } + } + + if (needsIceberg()) { + String extensions = session.conf().get(SparkLakeFormats.SPARK_CONF_EXTENSIONS, ""); + boolean extOk = + extensions != null + && extensions.toLowerCase(Locale.ROOT).contains("icebergsparksessionextensions"); + if (!extOk) { + throw new HopException( + "Active SparkSession is missing Iceberg extensions required for lake transforms. Set" + + " when launching (spark-submit --conf):\n" + + " " + + SparkLakeFormats.SPARK_CONF_EXTENSIONS + + "=" + + SparkLakeFormats.ICEBERG_EXTENSIONS + + "\nAlso --packages" + + " org.apache.iceberg:iceberg-spark-runtime-4.1_2.13:1.11.0." + + " Current spark.sql.extensions='" + + extensions + + "'."); + } + String pathCat = session.conf().get(SparkLakeFormats.SPARK_CONF_ICEBERG_PATH_CATALOG, ""); + if (StringUtils.isEmpty(pathCat)) { + if (log != null) { + log.logBasic( + "Registering Iceberg PATH catalog '" + + SparkLakeFormats.ICEBERG_PATH_CATALOG_NAME + + "' on active session"); + } + SparkLakeTableSupport.ensureIcebergPathCatalog(session); + } + } + + // Apply / verify user SparkCatalog metadata (TABLE mode) + for (Map.Entry e : catalogsByMetaName.entrySet()) { + SparkCatalog cat = e.getValue(); + String sparkName = StringUtils.defaultIfBlank(cat.getCatalogName(), cat.getName()); + if (variables != null) { + sparkName = variables.resolve(sparkName); + } + String confKey = "spark.sql.catalog." + sparkName; + String existing = session.conf().get(confKey, ""); + if (StringUtils.isEmpty(existing)) { + if (log != null) { + log.logBasic( + "Registering SparkCatalog '" + + e.getKey() + + "' (spark name=" + + sparkName + + ") on active session"); + } + try { + SparkCatalogApplier.applyToSession(session, cat, variables); + } catch (Exception ex) { + throw new HopException( + "Unable to register SparkCatalog '" + + e.getKey() + + "' on active SparkSession. Pass spark.sql.catalog." + + sparkName + + ".* via spark-submit --conf. Cause: " + + ex.getMessage(), + ex); + } + } + } + } + + static String defaultIcebergPathWarehouse() { + return Paths.get(System.getProperty("java.io.tmpdir"), "hop-iceberg-path-catalog-warehouse") + .toAbsolutePath() + .normalize() + .toUri() + .toString(); + } + + private static void loadMeta( + ITransformMeta meta, TransformMeta transformMeta, IHopMetadataProvider metadataProvider) + throws HopException { + try { + String xml = transformMeta.getXml(); + Node node = XmlHandler.getSubNode(XmlHandler.loadXmlString(xml), TransformMeta.XML_TAG); + meta.loadXml(node, metadataProvider); + } catch (Exception e) { + ITransformMeta live = transformMeta.getTransform(); + if (meta instanceof SparkLakeTableInputMeta target + && live instanceof SparkLakeTableInputMeta in) { + target.setFormat(in.getFormat()); + target.setIdentifierMode(in.getIdentifierMode()); + target.setTablePath(in.getTablePath()); + target.setTableIdentifier(in.getTableIdentifier()); + target.setCatalogMetadataName(in.getCatalogMetadataName()); + target.setTimeTravelType(in.getTimeTravelType()); + target.setTimeTravelVersion(in.getTimeTravelVersion()); + target.setTimeTravelTimestamp(in.getTimeTravelTimestamp()); + target.setExtraOptions(in.getExtraOptions()); + target.setFields(in.getFields()); + return; + } + if (meta instanceof SparkLakeTableOutputMeta target + && live instanceof SparkLakeTableOutputMeta out) { + target.setFormat(out.getFormat()); + target.setIdentifierMode(out.getIdentifierMode()); + target.setTablePath(out.getTablePath()); + target.setTableIdentifier(out.getTableIdentifier()); + target.setCatalogMetadataName(out.getCatalogMetadataName()); + target.setSaveMode(out.getSaveMode()); + target.setExtraOptions(out.getExtraOptions()); + target.setPartitionByColumns(out.getPartitionByColumns()); + target.setCoalescePartitions(out.getCoalescePartitions()); + return; + } + if (meta instanceof SparkLakeTableMergeMeta target + && live instanceof SparkLakeTableMergeMeta merge) { + target.setFormat(merge.getFormat()); + target.setIdentifierMode(merge.getIdentifierMode()); + target.setTablePath(merge.getTablePath()); + target.setTableIdentifier(merge.getTableIdentifier()); + target.setCatalogMetadataName(merge.getCatalogMetadataName()); + target.setMergeCondition(merge.getMergeCondition()); + target.setMatchedAction(merge.getMatchedAction()); + target.setNotMatchedAction(merge.getNotMatchedAction()); + target.setNotMatchedBySourceAction(merge.getNotMatchedBySourceAction()); + target.setRawMergeSql(merge.getRawMergeSql()); + return; + } + if (meta instanceof SparkLakeTableMaintenanceMeta target + && live instanceof SparkLakeTableMaintenanceMeta maint) { + target.setFormat(maint.getFormat()); + target.setIdentifierMode(maint.getIdentifierMode()); + target.setTablePath(maint.getTablePath()); + target.setTableIdentifier(maint.getTableIdentifier()); + target.setCatalogMetadataName(maint.getCatalogMetadataName()); + target.setOperation(maint.getOperation()); + target.setRetentionHours(maint.getRetentionHours()); + target.setRetainLast(maint.getRetainLast()); + target.setWhereClause(maint.getWhereClause()); + target.setZOrderColumns(maint.getZOrderColumns()); + target.setAcknowledgeDestructive(maint.isAcknowledgeDestructive()); + return; + } + throw new HopException( + "Unable to load lake transform metadata for '" + transformMeta.getName() + "'", e); + } + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/SparkCatalogApplier.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/SparkCatalogApplier.java new file mode 100644 index 00000000000..6d7870acba2 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/SparkCatalogApplier.java @@ -0,0 +1,189 @@ +/* + * 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.hop.spark.table; + +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.spark.metadata.SparkCatalog; +import org.apache.spark.sql.SparkSession; + +/** + * Expands {@link SparkCatalog} metadata into {@code spark.sql.catalog..*} configuration for + * hop-run builders and active sessions. + */ +public final class SparkCatalogApplier { + + private SparkCatalogApplier() {} + + /** + * Build Spark conf key/value pairs for a catalog definition. Does not log credential or confExtra + * values. + */ + public static Map toSparkConfigs(SparkCatalog catalog, IVariables variables) + throws HopException { + if (catalog == null) { + throw new HopException("SparkCatalog is null"); + } + String name = resolve(variables, catalog.getCatalogName()); + if (StringUtils.isEmpty(name)) { + // Fall back to Hop metadata entry name + name = resolve(variables, catalog.getName()); + } + if (StringUtils.isEmpty(name)) { + throw new HopException( + "SparkCatalog '" + + catalog.getName() + + "' has no Spark catalog name (catalogName / name)"); + } + if (!name.matches("[A-Za-z_][A-Za-z0-9_]*")) { + throw new HopException( + "Spark catalog name '" + + name + + "' is invalid (use letters, digits, underscore; start with letter/underscore)"); + } + + String type = + StringUtils.defaultIfBlank(catalog.getCatalogType(), SparkCatalog.TYPE_HADOOP) + .trim() + .toLowerCase(Locale.ROOT); + + Map conf = new LinkedHashMap<>(); + String prefix = "spark.sql.catalog." + name; + + String impl = resolve(variables, catalog.getImplementation()); + if (StringUtils.isEmpty(impl)) { + impl = SparkLakeFormats.ICEBERG_CATALOG; + } + + switch (type) { + case SparkCatalog.TYPE_HADOOP -> { + conf.put(prefix, impl); + conf.put(prefix + ".type", "hadoop"); + String warehouse = resolve(variables, catalog.getWarehouse()); + if (StringUtils.isEmpty(warehouse)) { + throw new HopException( + "SparkCatalog '" + catalog.getName() + "' (hadoop) requires a warehouse path/URI"); + } + conf.put(prefix + ".warehouse", toUriIfLocalPath(warehouse)); + } + case SparkCatalog.TYPE_REST -> { + conf.put(prefix, impl); + conf.put(prefix + ".type", "rest"); + String uri = resolve(variables, catalog.getUri()); + if (StringUtils.isEmpty(uri)) { + throw new HopException( + "SparkCatalog '" + catalog.getName() + "' (rest) requires a URI endpoint"); + } + conf.put(prefix + ".uri", uri); + String warehouse = resolve(variables, catalog.getWarehouse()); + if (StringUtils.isNotEmpty(warehouse)) { + conf.put(prefix + ".warehouse", toUriIfLocalPath(warehouse)); + } + } + case SparkCatalog.TYPE_CUSTOM -> { + conf.put(prefix, impl); + } + case SparkCatalog.TYPE_HIVE, SparkCatalog.TYPE_GLUE -> { + // Advanced: operator supplies full conf via confExtra / implementation + if (StringUtils.isNotEmpty(impl)) { + conf.put(prefix, impl); + } + conf.put(prefix + ".type", type); + } + default -> + throw new HopException( + "Unsupported SparkCatalog type '" + + type + + "' on '" + + catalog.getName() + + "'. Supported: hadoop, rest, custom, hive, glue."); + } + + // Optional credential — only if operator maps it via confExtra typically; expose as token + // property for REST when set + String credential = resolve(variables, catalog.getCredential()); + if (StringUtils.isNotEmpty(credential) && SparkCatalog.TYPE_REST.equals(type)) { + conf.putIfAbsent(prefix + ".token", credential); + } + + // confExtra: key=value lines → spark.sql.catalog..key=value + // (or full spark.* keys if they contain a dot prefix already starting with spark.) + String extra = catalog.getConfExtra(); + if (StringUtils.isNotEmpty(extra)) { + for (String line : extra.split("\\r?\\n")) { + String trimmed = line.trim(); + if (trimmed.isEmpty() || trimmed.startsWith("#")) { + continue; + } + int eq = trimmed.indexOf('='); + if (eq <= 0) { + continue; + } + String key = resolve(variables, trimmed.substring(0, eq).trim()); + String value = resolve(variables, trimmed.substring(eq + 1).trim()); + if (StringUtils.isEmpty(key)) { + continue; + } + if (key.startsWith("spark.")) { + conf.put(key, value); + } else { + conf.put(prefix + "." + key, value); + } + } + } + + return conf; + } + + public static void applyToBuilder( + SparkSession.Builder builder, SparkCatalog catalog, IVariables variables) + throws HopException { + for (Map.Entry e : toSparkConfigs(catalog, variables).entrySet()) { + builder.config(e.getKey(), e.getValue()); + } + } + + public static void applyToSession( + SparkSession session, SparkCatalog catalog, IVariables variables) throws HopException { + for (Map.Entry e : toSparkConfigs(catalog, variables).entrySet()) { + session.conf().set(e.getKey(), e.getValue()); + } + } + + private static String resolve(IVariables variables, String value) { + if (value == null) { + return null; + } + return variables != null ? variables.resolve(value) : value; + } + + static String toUriIfLocalPath(String path) { + if (StringUtils.isEmpty(path)) { + return path; + } + String p = path.trim(); + if (p.contains("://") || p.startsWith("file:")) { + return p; + } + return java.nio.file.Paths.get(p).toAbsolutePath().normalize().toUri().toString(); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/SparkLakeActionSupport.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/SparkLakeActionSupport.java new file mode 100644 index 00000000000..d41df3fe4ce --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/SparkLakeActionSupport.java @@ -0,0 +1,49 @@ +/* + * 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.hop.spark.table; + +import java.util.ArrayList; +import java.util.Map; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.types.DataTypes; +import org.apache.spark.sql.types.StructType; + +/** + * Shared action-sink helpers for lakehouse write/merge/maintenance handlers. Mirrors {@code + * SparkFileOutputHandler}: run the action during {@code handleTransform}, then register an empty + * leaf so a later engine {@code count()} does not re-commit. + */ +public final class SparkLakeActionSupport { + + public static final String EMPTY_LEAF_COLUMN = "_spark_hop_written"; + + private SparkLakeActionSupport() {} + + /** Empty Dataset registered as the transform output after a successful write action. */ + public static Dataset emptyLeaf(SparkSession spark) { + StructType emptySchema = new StructType().add(EMPTY_LEAF_COLUMN, DataTypes.BooleanType, false); + return spark.createDataFrame(new ArrayList<>(), emptySchema); + } + + public static void putEmptyLeaf( + Map> transformDatasetMap, String transformName, SparkSession spark) { + transformDatasetMap.put(transformName, emptyLeaf(spark)); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/SparkLakeConnectorProbe.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/SparkLakeConnectorProbe.java new file mode 100644 index 00000000000..ee167201c2a --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/SparkLakeConnectorProbe.java @@ -0,0 +1,159 @@ +/* + * 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.hop.spark.table; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import org.apache.hop.core.exception.HopException; + +/** + * Verifies that lakehouse connector classes are loadable from a given classloader (the native Spark + * engine plugin classloader for hop-run / GUI {@code local[*]}, or the driver classpath under + * spark-submit). + * + *

Default Hop assemblies ship Delta Lake and Apache Iceberg under {@code + * plugins/engines/spark/lib/delta/} and {@code lib/iceberg/}. If they are missing (custom install, + * stripped fat jar, or cluster without site jars), operators can restore those folders or use + * {@code spark-submit --packages}. See {@code plugins/engines/spark/README.md}. + */ +public final class SparkLakeConnectorProbe { + + private SparkLakeConnectorProbe() {} + + /** + * Ensures every requested format has its primary extension class on {@code classLoader}. + * + * @param formats format ids ({@link SparkLakeFormats#FORMAT_DELTA}, {@link + * SparkLakeFormats#FORMAT_ICEBERG}); null/blank entries ignored; unknown formats fail + * @param classLoader classloader that will load Spark session / DataSource classes (typically the + * engine plugin CL) + * @throws HopException if a required connector class is missing, with install / --packages recipe + */ + public static void verifyClasspath(Collection formats, ClassLoader classLoader) + throws HopException { + Objects.requireNonNull(classLoader, "classLoader"); + for (String format : normalizeFormats(formats)) { + switch (format) { + case SparkLakeFormats.FORMAT_DELTA -> + requireClass( + classLoader, + SparkLakeFormats.DELTA_EXTENSION, + missingDeltaMessage(SparkLakeFormats.DELTA_EXTENSION)); + case SparkLakeFormats.FORMAT_ICEBERG -> + requireClass( + classLoader, + SparkLakeFormats.ICEBERG_EXTENSIONS, + missingIcebergMessage(SparkLakeFormats.ICEBERG_EXTENSIONS)); + default -> + throw new HopException( + "Unsupported lakehouse table format '" + + format + + "'. Supported: " + + SparkLakeFormats.FORMAT_DELTA + + ", " + + SparkLakeFormats.FORMAT_ICEBERG + + "."); + } + } + } + + /** + * Like {@link #verifyClasspath(Collection, ClassLoader)} using the probe's own classloader + * (engine plugin CL when this class is loaded from the spark engine jar). + */ + public static void verifyClasspath(Collection formats) throws HopException { + verifyClasspath(formats, SparkLakeConnectorProbe.class.getClassLoader()); + } + + /** True if the given class is loadable (no initialization). */ + public static boolean isClassPresent(String className, ClassLoader classLoader) { + Objects.requireNonNull(className, "className"); + Objects.requireNonNull(classLoader, "classLoader"); + try { + Class.forName(className, false, classLoader); + return true; + } catch (ClassNotFoundException | NoClassDefFoundError e) { + return false; + } + } + + public static boolean isClassPresent(String className) { + return isClassPresent(className, SparkLakeConnectorProbe.class.getClassLoader()); + } + + public static boolean isDeltaPresent(ClassLoader classLoader) { + return isClassPresent(SparkLakeFormats.DELTA_EXTENSION, classLoader); + } + + public static boolean isIcebergPresent(ClassLoader classLoader) { + return isClassPresent(SparkLakeFormats.ICEBERG_EXTENSIONS, classLoader); + } + + /** + * Actionable message when the Delta connector is missing from the engine classpath. + * + *

Public for unit tests and for callers that want to surface the recipe without probing. + */ + public static String missingDeltaMessage(String missingClass) { + return "Delta Lake connector not on the native Spark engine classpath (missing class: " + + missingClass + + "). Default Hop installs ship it under plugins/engines/spark/lib/delta/ " + + "(delta-spark_4.1_2.13 and deps). Reinstall/rebuild the engines-spark plugin, or place " + + "the jars there and restart Hop. For spark-submit (if not using a fat jar that includes " + + "lib/delta): --packages io.delta:delta-spark_4.1_2.13:4.3.1 " + + "(see docs: pipeline/spark/lakehouse.html)."; + } + + /** Actionable message when the Iceberg connector is missing from the engine classpath. */ + public static String missingIcebergMessage(String missingClass) { + return "Apache Iceberg connector not on the native Spark engine classpath (missing class: " + + missingClass + + "). Default Hop installs ship it under plugins/engines/spark/lib/iceberg/ " + + "(iceberg-spark-runtime-4.1_2.13). Reinstall/rebuild the engines-spark plugin, or place " + + "the jar there and restart Hop. For spark-submit (if not using a fat jar that includes " + + "lib/iceberg): --packages org.apache.iceberg:iceberg-spark-runtime-4.1_2.13:1.11.0 " + + "(see docs: pipeline/spark/lakehouse.html)."; + } + + private static void requireClass(ClassLoader classLoader, String className, String message) + throws HopException { + if (!isClassPresent(className, classLoader)) { + throw new HopException(message); + } + } + + private static Set normalizeFormats(Collection formats) { + Set out = new LinkedHashSet<>(); + if (formats == null) { + return out; + } + for (String raw : formats) { + if (raw == null) { + continue; + } + String f = raw.trim().toLowerCase(Locale.ROOT); + if (!f.isEmpty()) { + out.add(f); + } + } + return out; + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/SparkLakeFormats.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/SparkLakeFormats.java new file mode 100644 index 00000000000..74296b1a9f7 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/SparkLakeFormats.java @@ -0,0 +1,62 @@ +/* + * 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.hop.spark.table; + +/** + * Format identifiers and well-known class / Spark conf names for open table formats on the native + * Spark engine. Connector JARs are optional at runtime (see {@link SparkLakeConnectorProbe}). + */ +public final class SparkLakeFormats { + + public static final String FORMAT_DELTA = "delta"; + public static final String FORMAT_ICEBERG = "iceberg"; + + /** Delta SparkSession extension (must be set at session build when Delta is used). */ + public static final String DELTA_EXTENSION = "io.delta.sql.DeltaSparkSessionExtension"; + + /** + * Delta catalog implementation for {@code spark.sql.catalog.spark_catalog}. Required for modern + * Delta 4.x on Spark 4 (SQL / MERGE / OPTIMIZE); hop-run always sets it when Delta is needed. + */ + public static final String DELTA_CATALOG = "org.apache.spark.sql.delta.catalog.DeltaCatalog"; + + public static final String SPARK_CONF_EXTENSIONS = "spark.sql.extensions"; + public static final String SPARK_CONF_SPARK_CATALOG = "spark.sql.catalog.spark_catalog"; + + /** Iceberg SparkSession extensions. */ + public static final String ICEBERG_EXTENSIONS = + "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions"; + + /** Default Iceberg catalog implementation (Hadoop / REST via catalog conf). */ + public static final String ICEBERG_CATALOG = "org.apache.iceberg.spark.SparkCatalog"; + + /** + * Built-in Hadoop catalog name used for Iceberg PATH mode ({@code hop_iceberg.`file:///…`}). + * Distinct from {@code spark_catalog} so Delta can keep DeltaCatalog when both formats co-exist. + */ + public static final String ICEBERG_PATH_CATALOG_NAME = "hop_iceberg"; + + public static final String SPARK_CONF_ICEBERG_PATH_CATALOG = + "spark.sql.catalog." + ICEBERG_PATH_CATALOG_NAME; + public static final String SPARK_CONF_ICEBERG_PATH_CATALOG_TYPE = + SPARK_CONF_ICEBERG_PATH_CATALOG + ".type"; + public static final String SPARK_CONF_ICEBERG_PATH_CATALOG_WAREHOUSE = + SPARK_CONF_ICEBERG_PATH_CATALOG + ".warehouse"; + + private SparkLakeFormats() {} +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/SparkLakeTableSupport.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/SparkLakeTableSupport.java new file mode 100644 index 00000000000..93415e124f8 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/SparkLakeTableSupport.java @@ -0,0 +1,877 @@ +/* + * 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.hop.spark.table; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.Const; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.spark.pipeline.handler.SparkFileInputHandler; +import org.apache.hop.spark.pipeline.handler.SparkFileIoSupport; +import org.apache.hop.spark.transforms.table.SparkLakeTableInputMeta; +import org.apache.hop.spark.transforms.table.SparkLakeTableOutputMeta; +import org.apache.hop.spark.util.SparkPathDialect; +import org.apache.spark.sql.DataFrameReader; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SaveMode; +import org.apache.spark.sql.SparkSession; + +/** + * Format × mode decision table for lake table I/O. + * + *

    + *
  • Delta PATH: {@code format("delta").load/save(path)} (requires DeltaCatalog on session) + *
  • Iceberg PATH: path identifier under built-in Hadoop catalog {@code hop_iceberg.`uri`} — + * bare {@code format("iceberg").load(path)} defaults to HiveCatalog and is not used + *
+ */ +public final class SparkLakeTableSupport { + + private SparkLakeTableSupport() {} + + public static String normalizeFormat(String format) throws HopException { + if (StringUtils.isEmpty(format)) { + return SparkLakeFormats.FORMAT_DELTA; + } + String f = format.trim().toLowerCase(Locale.ROOT); + if (SparkLakeFormats.FORMAT_DELTA.equals(f) || SparkLakeFormats.FORMAT_ICEBERG.equals(f)) { + return f; + } + throw new HopException( + "Unsupported lake table format '" + + format + + "'. Supported: " + + SparkLakeFormats.FORMAT_DELTA + + ", " + + SparkLakeFormats.FORMAT_ICEBERG + + "."); + } + + public static String normalizeIdentifierMode(String mode) throws HopException { + if (StringUtils.isEmpty(mode)) { + return SparkLakeTableInputMeta.MODE_PATH; + } + String m = mode.trim().toUpperCase(Locale.ROOT); + if (SparkLakeTableInputMeta.MODE_PATH.equals(m) + || SparkLakeTableInputMeta.MODE_TABLE.equals(m)) { + return m; + } + throw new HopException( + "Unsupported identifier mode '" + + mode + + "'. Supported: " + + SparkLakeTableInputMeta.MODE_PATH + + ", " + + SparkLakeTableInputMeta.MODE_TABLE + + "."); + } + + /** + * Normalize a filesystem path to a URI string suitable for Iceberg path identifiers ({@code + * file:///…}, {@code s3a://…}, etc.). + */ + public static String toTableLocationUri(String path) { + if (StringUtils.isEmpty(path)) { + return path; + } + String p = path.trim(); + if (p.contains("://") || p.startsWith("file:")) { + return p; + } + Path absolute = Paths.get(p).toAbsolutePath().normalize(); + return absolute.toUri().toString(); + } + + /** + * Spark SQL multi-part identifier for an Iceberg path-based table under the hop path catalog: + * {@code hop_iceberg.`file:///path/to/table`}. + */ + public static String icebergPathSqlIdentifier(String path) { + String uri = toTableLocationUri(path); + // Escape any backticks in the URI (unlikely) by doubling them for Spark SQL quoting + String escaped = uri.replace("`", "``"); + return SparkLakeFormats.ICEBERG_PATH_CATALOG_NAME + ".`" + escaped + "`"; + } + + public static String normalizeTimeTravelType(String type) throws HopException { + if (StringUtils.isEmpty(type)) { + return SparkLakeTableInputMeta.TIME_TRAVEL_NONE; + } + String t = type.trim().toUpperCase(Locale.ROOT); + if (SparkLakeTableInputMeta.TIME_TRAVEL_NONE.equals(t) + || SparkLakeTableInputMeta.TIME_TRAVEL_VERSION.equals(t) + || SparkLakeTableInputMeta.TIME_TRAVEL_TIMESTAMP.equals(t)) { + return t; + } + throw new HopException( + "Unsupported time travel type '" + + type + + "'. Supported: " + + SparkLakeTableInputMeta.TIME_TRAVEL_NONE + + ", " + + SparkLakeTableInputMeta.TIME_TRAVEL_VERSION + + ", " + + SparkLakeTableInputMeta.TIME_TRAVEL_TIMESTAMP + + "."); + } + + /** + * Maps UI time-travel settings to format-specific option keys (for unit tests and Delta reader). + * + *
    + *
  • Delta VERSION → {@code versionAsOf} + *
  • Delta TIMESTAMP → {@code timestampAsOf} + *
  • Iceberg VERSION → {@code snapshot-id} + *
  • Iceberg TIMESTAMP → {@code as-of-timestamp} (not applied via format reader for PATH; SQL + * clause is used instead) + *
+ */ + public static Map timeTravelOptionMap( + String format, String timeTravelType, String version, String timestamp) throws HopException { + Map map = new java.util.LinkedHashMap<>(); + String fmt = normalizeFormat(format); + String tt = normalizeTimeTravelType(timeTravelType); + if (SparkLakeTableInputMeta.TIME_TRAVEL_NONE.equals(tt)) { + return map; + } + if (SparkLakeTableInputMeta.TIME_TRAVEL_VERSION.equals(tt)) { + if (StringUtils.isEmpty(version)) { + throw new HopException("Time travel type VERSION requires a version / snapshot id value"); + } + if (SparkLakeFormats.FORMAT_DELTA.equals(fmt)) { + map.put("versionAsOf", version.trim()); + } else { + map.put("snapshot-id", version.trim()); + } + return map; + } + // TIMESTAMP + if (StringUtils.isEmpty(timestamp)) { + throw new HopException("Time travel type TIMESTAMP requires a timestamp value"); + } + if (SparkLakeFormats.FORMAT_DELTA.equals(fmt)) { + map.put("timestampAsOf", timestamp.trim()); + } else { + map.put("as-of-timestamp", timestamp.trim()); + } + return map; + } + + /** + * Resolve MERGE target SQL identifier for Delta/Iceberg PATH or TABLE mode. + * + *
    + *
  • Delta PATH → {@code delta.`path`} + *
  • Iceberg PATH → {@code hop_iceberg.`uri`} + *
  • TABLE → resolved multi-part table id + *
+ */ + public static String resolveMergeTargetSqlId( + SparkSession spark, + IVariables variables, + org.apache.hop.spark.transforms.table.SparkLakeTableMergeMeta meta, + String transformName) + throws HopException { + return resolveMergeTargetSqlId(spark, variables, meta, transformName, null); + } + + public static String resolveMergeTargetSqlId( + SparkSession spark, + IVariables variables, + org.apache.hop.spark.transforms.table.SparkLakeTableMergeMeta meta, + String transformName, + String pathSchemeMap) + throws HopException { + return resolveLakeTargetSqlId( + spark, + variables, + meta.getFormat(), + meta.getIdentifierMode(), + meta.getTablePath(), + meta.getTableIdentifier(), + transformName, + "Merge", + pathSchemeMap); + } + + /** + * Resolve maintenance target: SQL id for Delta/DELETE, plus Iceberg CALL catalog and table ref. + */ + public static MaintenanceTarget resolveMaintenanceTarget( + SparkSession spark, + IVariables variables, + org.apache.hop.spark.transforms.table.SparkLakeTableMaintenanceMeta meta, + String transformName) + throws HopException { + return resolveMaintenanceTarget(spark, variables, meta, transformName, null); + } + + public static MaintenanceTarget resolveMaintenanceTarget( + SparkSession spark, + IVariables variables, + org.apache.hop.spark.transforms.table.SparkLakeTableMaintenanceMeta meta, + String transformName, + String pathSchemeMap) + throws HopException { + String format = normalizeFormat(meta.getFormat()); + String mode = normalizeIdentifierMode(meta.getIdentifierMode()); + String sqlId = + resolveLakeTargetSqlId( + spark, + variables, + format, + mode, + meta.getTablePath(), + meta.getTableIdentifier(), + transformName, + "Maintenance", + pathSchemeMap); + + String procedureCatalog = null; + String tableRefForCall = null; + if (SparkLakeFormats.FORMAT_ICEBERG.equals(format)) { + if (SparkLakeTableInputMeta.MODE_PATH.equals(mode)) { + procedureCatalog = SparkLakeFormats.ICEBERG_PATH_CATALOG_NAME; + tableRefForCall = + toTableLocationUri( + SparkPathDialect.toSparkUri(variables.resolve(meta.getTablePath()), pathSchemeMap)); + } else { + String tableId = resolveTableIdentifier(meta.getTableIdentifier(), null, variables); + String[] parts = tableId.split("\\."); + if (parts.length >= 3) { + procedureCatalog = parts[0]; + StringBuilder rest = new StringBuilder(parts[1]); + for (int i = 2; i < parts.length; i++) { + rest.append('.').append(parts[i]); + } + tableRefForCall = rest.toString(); + } else if (parts.length == 2) { + procedureCatalog = SparkLakeFormats.ICEBERG_PATH_CATALOG_NAME; + tableRefForCall = tableId; + } else { + throw new HopException( + "Iceberg TABLE maintenance needs catalog.ns.table identifier, got: " + tableId); + } + } + } + return new MaintenanceTarget(sqlId, procedureCatalog, tableRefForCall, format); + } + + public static String resolveLakeTargetSqlId( + SparkSession spark, + IVariables variables, + String formatRaw, + String modeRaw, + String tablePath, + String tableIdentifier, + String transformName, + String role) + throws HopException { + return resolveLakeTargetSqlId( + spark, + variables, + formatRaw, + modeRaw, + tablePath, + tableIdentifier, + transformName, + role, + null); + } + + public static String resolveLakeTargetSqlId( + SparkSession spark, + IVariables variables, + String formatRaw, + String modeRaw, + String tablePath, + String tableIdentifier, + String transformName, + String role, + String pathSchemeMap) + throws HopException { + String format = normalizeFormat(formatRaw); + String mode = normalizeIdentifierMode(modeRaw); + + if (SparkLakeTableInputMeta.MODE_TABLE.equals(mode)) { + return resolveTableIdentifier(tableIdentifier, null, variables); + } + + String path = SparkPathDialect.toSparkUri(variables.resolve(tablePath), pathSchemeMap); + if (StringUtils.isEmpty(path)) { + throw new HopException( + "Spark Lake Table " + + role + + " '" + + transformName + + "' has no table path configured for PATH mode"); + } + + if (SparkLakeFormats.FORMAT_DELTA.equals(format)) { + String loc = path.replace("`", "``"); + return "delta.`" + loc + "`"; + } + + if (spark != null) { + ensureIcebergPathCatalog(spark); + } + return icebergPathSqlIdentifier(path); + } + + /** Maintenance target resolution result. */ + public record MaintenanceTarget( + String targetSqlId, String procedureCatalog, String tableRefForCall, String format) {} + + /** + * Resolve TABLE mode identifier (e.g. {@code lake.db.orders}). Optionally prefixes Spark catalog + * name from metadata when the identifier has only two parts. + */ + public static String resolveTableIdentifier( + String tableIdentifier, String sparkCatalogName, IVariables variables) throws HopException { + String id = variables != null ? variables.resolve(tableIdentifier) : tableIdentifier; + if (StringUtils.isEmpty(id)) { + throw new HopException("TABLE mode requires a table identifier (e.g. lake.db.orders)"); + } + id = id.trim(); + String cat = + variables != null && StringUtils.isNotEmpty(sparkCatalogName) + ? variables.resolve(sparkCatalogName) + : sparkCatalogName; + if (StringUtils.isNotEmpty(cat)) { + cat = cat.trim(); + // If id is ns.table (2 parts), prefix catalog name + long dots = id.chars().filter(ch -> ch == '.').count(); + if (dots == 1 && !id.startsWith(cat + ".")) { + id = cat + "." + id; + } + } + return id; + } + + /** Read a lake table as a Dataset (PATH + TABLE), with optional time travel. */ + public static Dataset resolveRead( + SparkSession spark, + IVariables variables, + ILogChannel log, + String transformName, + SparkLakeTableInputMeta meta) + throws HopException { + return resolveRead(spark, variables, log, transformName, meta, null); + } + + /** + * @param pathSchemeMap optional run-config path scheme map ({@code s3=s3a}); applied to PATH mode + * only + */ + public static Dataset resolveRead( + SparkSession spark, + IVariables variables, + ILogChannel log, + String transformName, + SparkLakeTableInputMeta meta, + String pathSchemeMap) + throws HopException { + + String format = normalizeFormat(meta.getFormat()); + String mode = normalizeIdentifierMode(meta.getIdentifierMode()); + Map options = + SparkFileIoSupport.parseExtraOptions(variables, meta.getExtraOptions()); + + String ttType = normalizeTimeTravelType(meta.getTimeTravelType()); + String ttVersion = + StringUtils.isNotEmpty(meta.getTimeTravelVersion()) + ? variables.resolve(meta.getTimeTravelVersion()) + : null; + String ttTimestamp = + StringUtils.isNotEmpty(meta.getTimeTravelTimestamp()) + ? variables.resolve(meta.getTimeTravelTimestamp()) + : null; + + Map ttOptions = timeTravelOptionMap(format, ttType, ttVersion, ttTimestamp); + options.putAll(ttOptions); + + Dataset dataset; + if (SparkLakeTableInputMeta.MODE_TABLE.equals(mode)) { + String tableId = resolveTableIdentifier(meta.getTableIdentifier(), null, variables); + // Prefer catalogMetadataName only for session plan; identifier is full Spark id + dataset = + readTable(spark, format, tableId, options, ttType, ttVersion, ttTimestamp, transformName); + } else { + String path = + SparkPathDialect.toSparkUri(variables.resolve(meta.getTablePath()), pathSchemeMap); + if (StringUtils.isEmpty(path)) { + throw new HopException( + "Spark Lake Table Input '" + transformName + "' has no table path configured"); + } + if (SparkLakeFormats.FORMAT_DELTA.equals(format)) { + dataset = readDeltaPath(spark, path, options, transformName); + } else { + dataset = readIcebergPath(spark, path, ttType, ttVersion, ttTimestamp, transformName); + } + } + + if (meta.getFields() != null && !meta.getFields().isEmpty()) { + dataset = + SparkFileInputHandler.projectAndCastByName(log, transformName, dataset, meta.getFields()); + } + return dataset; + } + + /** Write a Dataset to a lake table (PATH + TABLE). Action runs immediately. */ + public static void resolveWrite( + SparkSession spark, + Dataset dataset, + IVariables variables, + String transformName, + SparkLakeTableOutputMeta meta) + throws HopException { + resolveWrite(spark, dataset, variables, transformName, meta, null); + } + + /** + * @param pathSchemeMap optional run-config path scheme map ({@code s3=s3a}); applied to PATH mode + * only + */ + public static void resolveWrite( + SparkSession spark, + Dataset dataset, + IVariables variables, + String transformName, + SparkLakeTableOutputMeta meta, + String pathSchemeMap) + throws HopException { + + String format = normalizeFormat(meta.getFormat()); + String mode = normalizeIdentifierMode(meta.getIdentifierMode()); + Map options = + SparkFileIoSupport.parseExtraOptions(variables, meta.getExtraOptions()); + + SaveMode saveMode = SparkFileIoSupport.toSaveMode(meta.getSaveMode()); + if (StringUtils.isEmpty(meta.getSaveMode())) { + saveMode = SaveMode.ErrorIfExists; + } + + String[] partitionColumns = parsePartitionColumns(meta.getPartitionByColumns()); + + Integer coalesce = null; + if (StringUtils.isNotEmpty(meta.getCoalescePartitions())) { + int c = Const.toInt(variables.resolve(meta.getCoalescePartitions()), -1); + if (c > 0) { + coalesce = c; + } + } + + Dataset toWrite = dataset; + if (coalesce != null) { + toWrite = toWrite.coalesce(coalesce); + } + + if (SparkLakeTableInputMeta.MODE_TABLE.equals(mode)) { + String tableId = resolveTableIdentifier(meta.getTableIdentifier(), null, variables); + writeTable( + spark, toWrite, format, tableId, saveMode, options, partitionColumns, transformName); + return; + } + + String path = + SparkPathDialect.toSparkUri(variables.resolve(meta.getTablePath()), pathSchemeMap); + if (StringUtils.isEmpty(path)) { + throw new HopException( + "Spark Lake Table Output '" + transformName + "' has no table path configured"); + } + + if (SparkLakeFormats.FORMAT_DELTA.equals(format)) { + SparkFileIoSupport.writeDataset( + toWrite, SparkLakeFormats.FORMAT_DELTA, path, saveMode, options, partitionColumns, null); + } else { + writeIcebergPath(spark, toWrite, path, saveMode, partitionColumns, transformName); + } + } + + private static Dataset readTable( + SparkSession spark, + String format, + String tableId, + Map options, + String ttType, + String ttVersion, + String ttTimestamp, + String transformName) + throws HopException { + try { + if (SparkLakeFormats.FORMAT_ICEBERG.equals(format)) { + String sql = buildIcebergTimeTravelSql(tableId, ttType, ttVersion, ttTimestamp); + return spark.sql(sql); + } + // Delta TABLE — requires DeltaCatalog on spark_catalog + DataFrameReader reader = spark.read().format(SparkLakeFormats.FORMAT_DELTA); + for (Map.Entry e : options.entrySet()) { + reader = reader.option(e.getKey(), e.getValue()); + } + return reader.table(tableId); + } catch (Exception e) { + throw new HopException( + "Error reading " + + format + + " table '" + + tableId + + "' in transform '" + + transformName + + "'. For Iceberg ensure the Spark catalog is registered (SparkCatalog metadata /" + + " hop_iceberg). For Delta ensure spark_catalog=DeltaCatalog.", + e); + } + } + + private static void writeTable( + SparkSession spark, + Dataset dataset, + String format, + String tableId, + SaveMode saveMode, + Map options, + String[] partitionColumns, + String transformName) + throws HopException { + try { + if (SparkLakeFormats.FORMAT_ICEBERG.equals(format)) { + // Prefer writeTo (KD-24 spike choice for Iceberg TABLE) + switch (saveMode) { + case Overwrite -> + applyPartitioning( + dataset.writeTo(tableId).using(SparkLakeFormats.FORMAT_ICEBERG), + partitionColumns) + .createOrReplace(); + case Append -> { + try { + applyPartitioning(dataset.writeTo(tableId), partitionColumns).append(); + } catch (Exception appendEx) { + applyPartitioning( + dataset.writeTo(tableId).using(SparkLakeFormats.FORMAT_ICEBERG), + partitionColumns) + .create(); + } + } + case ErrorIfExists -> + applyPartitioning( + dataset.writeTo(tableId).using(SparkLakeFormats.FORMAT_ICEBERG), + partitionColumns) + .create(); + case Ignore -> { + try { + applyPartitioning( + dataset.writeTo(tableId).using(SparkLakeFormats.FORMAT_ICEBERG), + partitionColumns) + .create(); + } catch (Exception ignoreEx) { + if (!isTableAlreadyExists(ignoreEx)) { + throw ignoreEx; + } + } + } + default -> + throw new HopException("Unsupported save mode " + saveMode + " for Iceberg TABLE"); + } + return; + } + + // Delta TABLE (advanced): saveAsTable / insertInto via format writer + Dataset toWrite = dataset; + if (partitionColumns != null && partitionColumns.length > 0) { + // saveAsTable uses partitionBy on DataFrameWriter + } + var writer = toWrite.write().format(SparkLakeFormats.FORMAT_DELTA).mode(saveMode); + for (Map.Entry e : options.entrySet()) { + writer = writer.option(e.getKey(), e.getValue()); + } + if (partitionColumns != null && partitionColumns.length > 0) { + writer = writer.partitionBy(partitionColumns); + } + if (saveMode == SaveMode.Append) { + try { + writer.insertInto(tableId); + } catch (Exception insertEx) { + writer.saveAsTable(tableId); + } + } else { + writer.saveAsTable(tableId); + } + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException( + "Error writing " + + format + + " table '" + + tableId + + "' in transform '" + + transformName + + "'. Iceberg uses writeTo; Delta uses saveAsTable (requires DeltaCatalog).", + e); + } + } + + private static Dataset readDeltaPath( + SparkSession spark, String path, Map options, String transformName) + throws HopException { + DataFrameReader reader = spark.read().format(SparkLakeFormats.FORMAT_DELTA); + for (Map.Entry e : options.entrySet()) { + reader = reader.option(e.getKey(), e.getValue()); + } + try { + return reader.load(path); + } catch (Exception e) { + throw new HopException( + SparkPathDialect.withPathHint( + "Error reading Delta table at '" + + path + + "' in transform '" + + transformName + + "'. Ensure the Delta connector is on the engine classpath and the session has" + + " DeltaSparkSessionExtension + DeltaCatalog (see plugins/engines/spark/README.md).", + path), + e); + } + } + + private static Dataset readIcebergPath( + SparkSession spark, + String path, + String timeTravelType, + String version, + String timestamp, + String transformName) + throws HopException { + ensureIcebergPathCatalog(spark); + String sqlId = icebergPathSqlIdentifier(path); + String sql = buildIcebergTimeTravelSql(sqlId, timeTravelType, version, timestamp); + try { + return spark.sql(sql); + } catch (Exception e) { + throw new HopException( + SparkPathDialect.withPathHint( + "Error reading Iceberg table at '" + + path + + "' (sql=" + + sql + + ") in transform '" + + transformName + + "'. Ensure the Iceberg runtime is on the engine classpath and session has" + + " IcebergSparkSessionExtensions + Hadoop catalog '" + + SparkLakeFormats.ICEBERG_PATH_CATALOG_NAME + + "' (see plugins/engines/spark/README.md).", + path), + e); + } + } + + /** + * Builds {@code SELECT * FROM id [VERSION|TIMESTAMP AS OF …]} for Iceberg path identifiers. + * Package-visible for unit tests. + */ + static String buildIcebergTimeTravelSql( + String sqlIdentifier, String timeTravelType, String version, String timestamp) + throws HopException { + String tt = normalizeTimeTravelType(timeTravelType); + if (SparkLakeTableInputMeta.TIME_TRAVEL_NONE.equals(tt)) { + return "SELECT * FROM " + sqlIdentifier; + } + if (SparkLakeTableInputMeta.TIME_TRAVEL_VERSION.equals(tt)) { + if (StringUtils.isEmpty(version)) { + throw new HopException("Iceberg VERSION time travel requires a snapshot id"); + } + // Snapshot ids are numeric; pass as bare token (no quotes) so Spark parses as long + String snap = version.trim(); + if (!snap.matches("-?\\d+")) { + // Allow quoted non-numeric only if user provided quotes; otherwise quote string + return "SELECT * FROM " + sqlIdentifier + " VERSION AS OF " + snap; + } + return "SELECT * FROM " + sqlIdentifier + " VERSION AS OF " + snap; + } + if (StringUtils.isEmpty(timestamp)) { + throw new HopException("Iceberg TIMESTAMP time travel requires a timestamp value"); + } + String ts = timestamp.trim().replace("'", "''"); + return "SELECT * FROM " + sqlIdentifier + " TIMESTAMP AS OF TIMESTAMP '" + ts + "'"; + } + + private static void writeIcebergPath( + SparkSession spark, + Dataset dataset, + String path, + SaveMode saveMode, + String[] partitionColumns, + String transformName) + throws HopException { + ensureIcebergPathCatalog(spark); + String sqlId = icebergPathSqlIdentifier(path); + + try { + switch (saveMode) { + case Overwrite -> + applyPartitioning( + dataset.writeTo(sqlId).using(SparkLakeFormats.FORMAT_ICEBERG), partitionColumns) + .createOrReplace(); + case Append -> { + try { + applyPartitioning(dataset.writeTo(sqlId), partitionColumns).append(); + } catch (Exception appendEx) { + // Table may not exist yet — create + try { + applyPartitioning( + dataset.writeTo(sqlId).using(SparkLakeFormats.FORMAT_ICEBERG), + partitionColumns) + .create(); + } catch (Exception createEx) { + throw new HopException( + SparkPathDialect.withPathHint( + "Iceberg append failed for '" + + path + + "' and create fallback also failed in transform '" + + transformName + + "'", + path), + appendEx); + } + } + } + case ErrorIfExists -> + applyPartitioning( + dataset.writeTo(sqlId).using(SparkLakeFormats.FORMAT_ICEBERG), partitionColumns) + .create(); + case Ignore -> { + try { + applyPartitioning( + dataset.writeTo(sqlId).using(SparkLakeFormats.FORMAT_ICEBERG), partitionColumns) + .create(); + } catch (Exception ignoreEx) { + if (!isTableAlreadyExists(ignoreEx)) { + throw ignoreEx; + } + } + } + default -> + throw new HopException( + "Unsupported save mode " + + saveMode + + " for Iceberg PATH in transform '" + + transformName + + "'"); + } + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException( + SparkPathDialect.withPathHint( + "Error writing Iceberg table at '" + + path + + "' (identifier " + + sqlId + + ") in transform '" + + transformName + + "'. Ensure Iceberg is on the classpath and hop_iceberg Hadoop catalog is" + + " configured (see plugins/engines/spark/README.md).", + path), + e); + } + } + + private static org.apache.spark.sql.DataFrameWriterV2 applyPartitioning( + org.apache.spark.sql.DataFrameWriterV2 writer, String[] partitionColumns) { + if (partitionColumns == null || partitionColumns.length == 0) { + return writer; + } + org.apache.spark.sql.Column first = org.apache.spark.sql.functions.col(partitionColumns[0]); + if (partitionColumns.length == 1) { + return writer.partitionedBy(first); + } + org.apache.spark.sql.Column[] rest = + new org.apache.spark.sql.Column[partitionColumns.length - 1]; + for (int i = 1; i < partitionColumns.length; i++) { + rest[i - 1] = org.apache.spark.sql.functions.col(partitionColumns[i]); + } + return writer.partitionedBy(first, rest); + } + + /** + * Ensure the built-in Hadoop catalog for path identifiers is registered on this session. Safe to + * call multiple times; no-ops when already present. + */ + public static void ensureIcebergPathCatalog(SparkSession spark) { + String existing = spark.conf().get(SparkLakeFormats.SPARK_CONF_ICEBERG_PATH_CATALOG, ""); + if (StringUtils.isNotEmpty(existing)) { + return; + } + String warehouse = + Paths.get(System.getProperty("java.io.tmpdir"), "hop-iceberg-path-catalog-warehouse") + .toAbsolutePath() + .normalize() + .toUri() + .toString(); + spark + .conf() + .set(SparkLakeFormats.SPARK_CONF_ICEBERG_PATH_CATALOG, SparkLakeFormats.ICEBERG_CATALOG); + spark.conf().set(SparkLakeFormats.SPARK_CONF_ICEBERG_PATH_CATALOG_TYPE, "hadoop"); + spark.conf().set(SparkLakeFormats.SPARK_CONF_ICEBERG_PATH_CATALOG_WAREHOUSE, warehouse); + } + + private static String[] parsePartitionColumns(String partitionByColumns) { + if (StringUtils.isEmpty(partitionByColumns)) { + return null; + } + List parts = new ArrayList<>(); + for (String p : partitionByColumns.split(",")) { + if (StringUtils.isNotEmpty(p.trim())) { + parts.add(p.trim()); + } + } + return parts.isEmpty() ? null : parts.toArray(new String[0]); + } + + private static boolean isTableAlreadyExists(Throwable e) { + Throwable t = e; + while (t != null) { + String name = t.getClass().getSimpleName(); + String msg = t.getMessage() != null ? t.getMessage() : ""; + if (name.contains("TableAlreadyExists") + || msg.contains("TABLE_OR_VIEW_ALREADY_EXISTS") + || msg.toLowerCase(Locale.ROOT).contains("already exists")) { + return true; + } + t = t.getCause(); + } + return false; + } + + /** Formats referenced by a lake input/output meta (for session planning). */ + public static void collectFormat(String format, java.util.Set into) throws HopException { + if (StringUtils.isEmpty(format)) { + into.add(SparkLakeFormats.FORMAT_DELTA); + return; + } + into.add(normalizeFormat(format)); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/SparkMaintenanceSqlBuilder.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/SparkMaintenanceSqlBuilder.java new file mode 100644 index 00000000000..9b370d3e6d4 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/SparkMaintenanceSqlBuilder.java @@ -0,0 +1,257 @@ +/* + * 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.hop.spark.table; + +import java.util.Locale; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.exception.HopException; + +/** + * Builds Spark SQL for lakehouse maintenance (OPTIMIZE / VACUUM / expire / rewrite / DELETE). Does + * not execute SQL. + */ +public final class SparkMaintenanceSqlBuilder { + + public static final String OP_OPTIMIZE = "OPTIMIZE"; + public static final String OP_VACUUM = "VACUUM"; + public static final String OP_EXPIRE_SNAPSHOTS = "EXPIRE_SNAPSHOTS"; + public static final String OP_REWRITE_MANIFESTS = "REWRITE_MANIFESTS"; + public static final String OP_DELETE_WHERE = "DELETE_WHERE"; + + private SparkMaintenanceSqlBuilder() {} + + public static String normalizeOperation(String operation) throws HopException { + if (StringUtils.isEmpty(operation)) { + throw new HopException("Maintenance operation is required"); + } + String op = operation.trim().toUpperCase(Locale.ROOT); + return switch (op) { + case OP_OPTIMIZE, "COMPACT", "REWRITE_DATA_FILES" -> OP_OPTIMIZE; + case OP_VACUUM -> OP_VACUUM; + case OP_EXPIRE_SNAPSHOTS, "EXPIRE" -> OP_EXPIRE_SNAPSHOTS; + case OP_REWRITE_MANIFESTS -> OP_REWRITE_MANIFESTS; + case OP_DELETE_WHERE, "DELETE" -> OP_DELETE_WHERE; + default -> + throw new HopException( + "Unsupported maintenance operation '" + + operation + + "'. Supported: OPTIMIZE, VACUUM, EXPIRE_SNAPSHOTS, REWRITE_MANIFESTS," + + " DELETE_WHERE"); + }; + } + + /** + * @param format delta or iceberg + * @param targetSqlId SQL table id (e.g. {@code delta.`/p`} or {@code lake.db.t}) + * @param procedureCatalog catalog name for Iceberg CALL (e.g. {@code hop_iceberg} or {@code + * lake}); required for Iceberg CALL ops + * @param tableRefForCall table reference string for CALL table => '…' (path URI or ns.table) + * @param operation normalized operation + * @param retentionHours required for VACUUM / EXPIRE_SNAPSHOTS + * @param whereClause optional WHERE for OPTIMIZE / DELETE + * @param zOrderColumns optional comma-separated ZORDER columns (Delta OPTIMIZE only) + * @param retainLast optional retain_last for Iceberg expire (default 1 if blank) + */ + public static String build( + String format, + String targetSqlId, + String procedureCatalog, + String tableRefForCall, + String operation, + String retentionHours, + String whereClause, + String zOrderColumns, + String retainLast) + throws HopException { + + String fmt = SparkLakeTableSupport.normalizeFormat(format); + String op = normalizeOperation(operation); + + if (StringUtils.isEmpty(targetSqlId) && !isIcebergCall(op, fmt)) { + throw new HopException("Maintenance target table identifier is empty"); + } + + return switch (op) { + case OP_OPTIMIZE -> + buildOptimize( + fmt, targetSqlId, procedureCatalog, tableRefForCall, whereClause, zOrderColumns); + case OP_VACUUM -> buildVacuum(fmt, targetSqlId, retentionHours); + case OP_EXPIRE_SNAPSHOTS -> + buildExpireSnapshots(fmt, procedureCatalog, tableRefForCall, retentionHours, retainLast); + case OP_REWRITE_MANIFESTS -> buildRewriteManifests(fmt, procedureCatalog, tableRefForCall); + case OP_DELETE_WHERE -> buildDeleteWhere(targetSqlId, whereClause); + default -> throw new HopException("Unhandled operation " + op); + }; + } + + public static boolean requiresDestructiveAck(String operation) throws HopException { + String op = normalizeOperation(operation); + return OP_VACUUM.equals(op) || OP_EXPIRE_SNAPSHOTS.equals(op) || OP_DELETE_WHERE.equals(op); + } + + private static boolean isIcebergCall(String op, String format) { + return SparkLakeFormats.FORMAT_ICEBERG.equals(format) + && (OP_OPTIMIZE.equals(op) + || OP_EXPIRE_SNAPSHOTS.equals(op) + || OP_REWRITE_MANIFESTS.equals(op)); + } + + private static String buildOptimize( + String format, + String targetSqlId, + String procedureCatalog, + String tableRefForCall, + String whereClause, + String zOrderColumns) + throws HopException { + if (SparkLakeFormats.FORMAT_DELTA.equals(format)) { + StringBuilder sql = new StringBuilder("OPTIMIZE ").append(targetSqlId); + if (StringUtils.isNotEmpty(whereClause)) { + sql.append(" WHERE ").append(whereClause.trim()); + } + if (StringUtils.isNotEmpty(zOrderColumns)) { + sql.append(" ZORDER BY (").append(zOrderColumns.trim()).append(')'); + } + return sql.toString(); + } + // Iceberg compact = rewrite_data_files + requireCallArgs(procedureCatalog, tableRefForCall, "OPTIMIZE/rewrite_data_files"); + return "CALL " + + procedureCatalog + + ".system.rewrite_data_files(table => '" + + escapeSqlLiteral(tableRefForCall) + + "')"; + } + + private static String buildVacuum(String format, String targetSqlId, String retentionHours) + throws HopException { + if (!SparkLakeFormats.FORMAT_DELTA.equals(format)) { + throw new HopException( + "VACUUM is Delta-only. For Iceberg use EXPIRE_SNAPSHOTS (and optionally remove orphan" + + " files via advanced SQL)."); + } + double hours = parsePositiveRetentionHours(retentionHours, "VACUUM"); + return "VACUUM " + targetSqlId + " RETAIN " + formatHours(hours) + " HOURS"; + } + + private static String buildExpireSnapshots( + String format, + String procedureCatalog, + String tableRefForCall, + String retentionHours, + String retainLast) + throws HopException { + if (!SparkLakeFormats.FORMAT_ICEBERG.equals(format)) { + throw new HopException("EXPIRE_SNAPSHOTS is Iceberg-only. For Delta use VACUUM."); + } + requireCallArgs(procedureCatalog, tableRefForCall, "EXPIRE_SNAPSHOTS"); + // Prefer retain_last for simplicity; if retentionHours set, use older_than interval via + // timestamp expression is complex — require retain_last or retentionHours mapped to retain. + int retain = 1; + if (StringUtils.isNotEmpty(retainLast)) { + try { + retain = Integer.parseInt(retainLast.trim()); + } catch (NumberFormatException e) { + throw new HopException("retainLast must be an integer >= 1", e); + } + } + if (retain < 1) { + throw new HopException("retainLast must be >= 1 for EXPIRE_SNAPSHOTS"); + } + // Always require explicit retention policy: either retainLast was set or retentionHours + if (StringUtils.isEmpty(retainLast) && StringUtils.isEmpty(retentionHours)) { + throw new HopException( + "EXPIRE_SNAPSHOTS requires retainLast (>=1) and/or retentionHours (documented; retainLast" + + " is applied to CALL retain_last)"); + } + if (StringUtils.isNotEmpty(retentionHours)) { + // Validate positive; Iceberg CALL uses retain_last primarily in v1 + parsePositiveRetentionHours(retentionHours, "EXPIRE_SNAPSHOTS"); + } + return "CALL " + + procedureCatalog + + ".system.expire_snapshots(table => '" + + escapeSqlLiteral(tableRefForCall) + + "', retain_last => " + + retain + + ")"; + } + + private static String buildRewriteManifests( + String format, String procedureCatalog, String tableRefForCall) throws HopException { + if (!SparkLakeFormats.FORMAT_ICEBERG.equals(format)) { + throw new HopException("REWRITE_MANIFESTS is Iceberg-only"); + } + requireCallArgs(procedureCatalog, tableRefForCall, "REWRITE_MANIFESTS"); + return "CALL " + + procedureCatalog + + ".system.rewrite_manifests(table => '" + + escapeSqlLiteral(tableRefForCall) + + "')"; + } + + private static String buildDeleteWhere(String targetSqlId, String whereClause) + throws HopException { + if (StringUtils.isEmpty(targetSqlId)) { + throw new HopException("DELETE_WHERE requires a target table"); + } + if (StringUtils.isEmpty(whereClause)) { + throw new HopException("DELETE_WHERE requires a WHERE clause (refusing unrestricted DELETE)"); + } + return "DELETE FROM " + targetSqlId + " WHERE " + whereClause.trim(); + } + + private static void requireCallArgs(String catalog, String tableRef, String op) + throws HopException { + if (StringUtils.isEmpty(catalog) || !catalog.matches("[A-Za-z_][A-Za-z0-9_]*")) { + throw new HopException( + op + " requires a Spark procedure catalog name (e.g. hop_iceberg or lake)"); + } + if (StringUtils.isEmpty(tableRef)) { + throw new HopException(op + " requires a table reference for CALL table => '…'"); + } + } + + private static double parsePositiveRetentionHours(String retentionHours, String op) + throws HopException { + if (StringUtils.isEmpty(retentionHours)) { + throw new HopException( + op + " requires retentionHours (no silent default — refuse destructive ops without it)"); + } + try { + double h = Double.parseDouble(retentionHours.trim()); + if (h <= 0) { + throw new HopException(op + " retentionHours must be > 0"); + } + return h; + } catch (NumberFormatException e) { + throw new HopException(op + " retentionHours must be a number: " + retentionHours, e); + } + } + + private static String formatHours(double hours) { + if (hours == Math.rint(hours)) { + return Long.toString((long) hours); + } + return Double.toString(hours); + } + + static String escapeSqlLiteral(String value) { + return value.replace("'", "''"); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/SparkMergeSqlBuilder.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/SparkMergeSqlBuilder.java new file mode 100644 index 00000000000..010a9e6a2a4 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/table/SparkMergeSqlBuilder.java @@ -0,0 +1,171 @@ +/* + * 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.hop.spark.table; + +import java.util.Locale; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.exception.HopException; + +/** + * Builds Spark SQL {@code MERGE INTO} statements for lakehouse upserts. Does not execute SQL. + * + *

Target and source identifiers are inserted as provided (already resolved/quoted by the + * caller). The merge condition is operator-authored SQL and is not escaped beyond basic emptiness + * checks. + */ +public final class SparkMergeSqlBuilder { + + public static final String MATCHED_UPDATE_ALL = "UPDATE_ALL"; + public static final String MATCHED_DELETE = "DELETE"; + public static final String MATCHED_NONE = "NONE"; + + public static final String NOT_MATCHED_INSERT_ALL = "INSERT_ALL"; + public static final String NOT_MATCHED_NONE = "NONE"; + + public static final String NOT_MATCHED_BY_SOURCE_DELETE = "DELETE"; + public static final String NOT_MATCHED_BY_SOURCE_NONE = "NONE"; + + private SparkMergeSqlBuilder() {} + + /** + * @param targetSqlId target table SQL identifier (e.g. {@code delta.`/path`} or {@code + * lake.db.t}) + * @param sourceViewName temp view name for source rows (unquoted identifier) + * @param mergeCondition ON clause body (e.g. {@code t.id = s.id}) + * @param matchedAction {@link #MATCHED_UPDATE_ALL}, {@link #MATCHED_DELETE}, or {@link + * #MATCHED_NONE} + * @param notMatchedAction {@link #NOT_MATCHED_INSERT_ALL} or {@link #NOT_MATCHED_NONE} + * @param notMatchedBySourceAction {@link #NOT_MATCHED_BY_SOURCE_DELETE} or {@link + * #NOT_MATCHED_BY_SOURCE_NONE} (Delta/Iceberg support varies) + */ + public static String build( + String targetSqlId, + String sourceViewName, + String mergeCondition, + String matchedAction, + String notMatchedAction, + String notMatchedBySourceAction) + throws HopException { + + if (StringUtils.isEmpty(targetSqlId)) { + throw new HopException("MERGE target table identifier is empty"); + } + if (StringUtils.isEmpty(sourceViewName) || !isSafeSqlIdentifier(sourceViewName)) { + throw new HopException( + "MERGE source view name is invalid (use letters, digits, underscore): " + sourceViewName); + } + if (StringUtils.isEmpty(mergeCondition)) { + throw new HopException("MERGE condition (ON clause) is required"); + } + + String matched = normalizeMatched(matchedAction); + String notMatched = normalizeNotMatched(notMatchedAction); + String notMatchedBySource = normalizeNotMatchedBySource(notMatchedBySourceAction); + + if (MATCHED_NONE.equals(matched) + && NOT_MATCHED_NONE.equals(notMatched) + && NOT_MATCHED_BY_SOURCE_NONE.equals(notMatchedBySource)) { + throw new HopException( + "MERGE requires at least one action (matched UPDATE/DELETE and/or not-matched INSERT)"); + } + + StringBuilder sql = new StringBuilder(); + sql.append("MERGE INTO ") + .append(targetSqlId) + .append(" AS t\n") + .append("USING ") + .append(sourceViewName) + .append(" AS s\n") + .append("ON ") + .append(mergeCondition.trim()) + .append('\n'); + + if (MATCHED_UPDATE_ALL.equals(matched)) { + sql.append("WHEN MATCHED THEN UPDATE SET *\n"); + } else if (MATCHED_DELETE.equals(matched)) { + sql.append("WHEN MATCHED THEN DELETE\n"); + } + + if (NOT_MATCHED_INSERT_ALL.equals(notMatched)) { + sql.append("WHEN NOT MATCHED THEN INSERT *\n"); + } + + if (NOT_MATCHED_BY_SOURCE_DELETE.equals(notMatchedBySource)) { + // Supported on recent Delta; Iceberg support depends on version + sql.append("WHEN NOT MATCHED BY SOURCE THEN DELETE\n"); + } + + return sql.toString().trim(); + } + + /** Sanitize transform name into a temp-view identifier. */ + public static String sourceViewName(String transformName) { + String base = StringUtils.defaultIfBlank(transformName, "src").replaceAll("[^A-Za-z0-9_]", "_"); + if (base.isEmpty() || Character.isDigit(base.charAt(0))) { + base = "s_" + base; + } + return "hop_merge_src_" + base; + } + + public static boolean isSafeSqlIdentifier(String name) { + return name != null && name.matches("[A-Za-z_][A-Za-z0-9_]*"); + } + + private static String normalizeMatched(String action) throws HopException { + if (StringUtils.isEmpty(action)) { + return MATCHED_UPDATE_ALL; + } + String a = action.trim().toUpperCase(Locale.ROOT); + return switch (a) { + case MATCHED_UPDATE_ALL, "UPDATE", "UPDATESET", "UPDATE_SET" -> MATCHED_UPDATE_ALL; + case MATCHED_DELETE -> MATCHED_DELETE; + case MATCHED_NONE, "SKIP" -> MATCHED_NONE; + default -> + throw new HopException( + "Unsupported matched action '" + action + "'. Supported: UPDATE_ALL, DELETE, NONE"); + }; + } + + private static String normalizeNotMatched(String action) throws HopException { + if (StringUtils.isEmpty(action)) { + return NOT_MATCHED_INSERT_ALL; + } + String a = action.trim().toUpperCase(Locale.ROOT); + return switch (a) { + case NOT_MATCHED_INSERT_ALL, "INSERT", "INSERTSET", "INSERT_SET" -> NOT_MATCHED_INSERT_ALL; + case NOT_MATCHED_NONE, "SKIP" -> NOT_MATCHED_NONE; + default -> + throw new HopException( + "Unsupported not-matched action '" + action + "'. Supported: INSERT_ALL, NONE"); + }; + } + + private static String normalizeNotMatchedBySource(String action) throws HopException { + if (StringUtils.isEmpty(action)) { + return NOT_MATCHED_BY_SOURCE_NONE; + } + String a = action.trim().toUpperCase(Locale.ROOT); + return switch (a) { + case NOT_MATCHED_BY_SOURCE_DELETE -> NOT_MATCHED_BY_SOURCE_DELETE; + case NOT_MATCHED_BY_SOURCE_NONE, "SKIP" -> NOT_MATCHED_BY_SOURCE_NONE; + default -> + throw new HopException( + "Unsupported not-matched-by-source action '" + action + "'. Supported: DELETE, NONE"); + }; + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkField.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkField.java new file mode 100644 index 00000000000..1551633edf9 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkField.java @@ -0,0 +1,72 @@ +/* + * 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.hop.spark.transforms.io; + +import java.io.Serializable; +import lombok.Getter; +import lombok.Setter; +import org.apache.hop.core.exception.HopPluginException; +import org.apache.hop.core.row.IValueMeta; +import org.apache.hop.core.row.value.ValueMetaFactory; +import org.apache.hop.metadata.api.HopMetadataProperty; + +/** Field definition for Spark File Input schema. */ +@Getter +@Setter +public class SparkField implements Serializable { + private static final long serialVersionUID = 1L; + + @HopMetadataProperty(key = "name", injectionKey = "NAME") + private String name; + + /** Hop type description, e.g. String, Integer, Number, Boolean, Date, BigNumber, Binary */ + @HopMetadataProperty(key = "type", injectionKey = "TYPE") + private String hopType = "String"; + + @HopMetadataProperty(key = "length", injectionKey = "LENGTH") + private int length = -1; + + @HopMetadataProperty(key = "precision", injectionKey = "PRECISION") + private int precision = -1; + + @HopMetadataProperty(key = "format", injectionKey = "FORMAT_MASK") + private String formatMask; + + public SparkField() {} + + public SparkField(String name, String hopType) { + this.name = name; + this.hopType = hopType; + } + + public SparkField(String name, String hopType, int length, int precision) { + this.name = name; + this.hopType = hopType; + this.length = length; + this.precision = precision; + } + + public IValueMeta createValueMeta() throws HopPluginException { + int type = ValueMetaFactory.getIdForValueMeta(hopType); + IValueMeta valueMeta = ValueMetaFactory.createValueMeta(name, type, length, precision); + if (formatMask != null && !formatMask.isEmpty()) { + valueMeta.setConversionMask(formatMask); + } + return valueMeta; + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileInput.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileInput.java new file mode 100644 index 00000000000..0e2253bf21c --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileInput.java @@ -0,0 +1,48 @@ +/* + * 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.hop.spark.transforms.io; + +import org.apache.hop.core.exception.HopException; +import org.apache.hop.pipeline.Pipeline; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.BaseTransform; +import org.apache.hop.pipeline.transform.TransformMeta; + +/** + * Metadata-only transform on Local engine. On the native Spark engine it is converted to a Spark + * file read. + */ +public class SparkFileInput extends BaseTransform { + + public SparkFileInput( + TransformMeta transformMeta, + SparkFileInputMeta meta, + SparkFileInputData data, + int copyNr, + PipelineMeta pipelineMeta, + Pipeline pipeline) { + super(transformMeta, meta, data, copyNr, pipelineMeta, pipeline); + } + + @Override + public boolean processRow() throws HopException { + // Outside of Spark this transform is metadata only + setOutputDone(); + return false; + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileInputData.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileInputData.java new file mode 100644 index 00000000000..261fff0b958 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileInputData.java @@ -0,0 +1,27 @@ +/* + * 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.hop.spark.transforms.io; + +import org.apache.hop.pipeline.transform.BaseTransformData; +import org.apache.hop.pipeline.transform.ITransformData; + +public class SparkFileInputData extends BaseTransformData implements ITransformData { + public SparkFileInputData() { + super(); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileInputDialog.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileInputDialog.java new file mode 100644 index 00000000000..21b47f1ec07 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileInputDialog.java @@ -0,0 +1,323 @@ +/* + * 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.hop.spark.transforms.io; + +import java.util.ArrayList; +import java.util.List; +import org.apache.hop.core.Const; +import org.apache.hop.core.row.value.ValueMetaFactory; +import org.apache.hop.core.util.Utils; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.i18n.BaseMessages; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.ui.core.PropsUi; +import org.apache.hop.ui.core.dialog.BaseDialog; +import org.apache.hop.ui.core.widget.ColumnInfo; +import org.apache.hop.ui.core.widget.TableView; +import org.apache.hop.ui.core.widget.TextVar; +import org.apache.hop.ui.pipeline.transform.BaseTransformDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.CCombo; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.TableItem; +import org.eclipse.swt.widgets.Text; + +public class SparkFileInputDialog extends BaseTransformDialog { + private static final Class PKG = SparkFileInputMeta.class; + + private final SparkFileInputMeta input; + + private TextVar wFilePath; + private CCombo wFormat; + private Button wHeader; + private TextVar wSeparator; + private TextVar wQuote; + private Button wInferSchema; + private Text wExtraOptions; + private TableView wFields; + + public SparkFileInputDialog( + Shell parent, + IVariables variables, + SparkFileInputMeta transformMeta, + PipelineMeta pipelineMeta) { + super(parent, variables, transformMeta, pipelineMeta); + this.input = transformMeta; + } + + @Override + public String open() { + Shell parent = getParent(); + shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.RESIZE | SWT.MIN | SWT.MAX); + PropsUi.setLook(shell); + setShellImage(shell, input); + + ModifyListener lsMod = e -> input.setChanged(); + changed = input.hasChanged(); + + FormLayout formLayout = new FormLayout(); + formLayout.marginWidth = PropsUi.getFormMargin(); + formLayout.marginHeight = PropsUi.getFormMargin(); + shell.setLayout(formLayout); + shell.setText(BaseMessages.getString(PKG, "SparkFileInputDialog.Shell.Title")); + + int middle = props.getMiddlePct(); + int margin = PropsUi.getMargin(); + + // Transform name + wlTransformName = new Label(shell, SWT.RIGHT); + wlTransformName.setText(BaseMessages.getString(PKG, "System.Label.TransformName")); + PropsUi.setLook(wlTransformName); + fdlTransformName = new FormData(); + fdlTransformName.left = new FormAttachment(0, 0); + fdlTransformName.top = new FormAttachment(0, margin); + fdlTransformName.right = new FormAttachment(middle, -margin); + wlTransformName.setLayoutData(fdlTransformName); + wTransformName = new Text(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + wTransformName.setText(transformName); + PropsUi.setLook(wTransformName); + wTransformName.addModifyListener(lsMod); + fdTransformName = new FormData(); + fdTransformName.left = new FormAttachment(middle, 0); + fdTransformName.top = new FormAttachment(wlTransformName, 0, SWT.CENTER); + fdTransformName.right = new FormAttachment(100, 0); + wTransformName.setLayoutData(fdTransformName); + Control last = wTransformName; + + wFilePath = labeledTextVar(lsMod, middle, margin, last, "SparkFileInputDialog.FilePath"); + last = wFilePath; + + // Format + Label wlFormat = new Label(shell, SWT.RIGHT); + wlFormat.setText(BaseMessages.getString(PKG, "SparkFileInputDialog.Format")); + PropsUi.setLook(wlFormat); + FormData fdlFormat = new FormData(); + fdlFormat.left = new FormAttachment(0, 0); + fdlFormat.top = new FormAttachment(last, margin); + fdlFormat.right = new FormAttachment(middle, -margin); + wlFormat.setLayoutData(fdlFormat); + wFormat = new CCombo(shell, SWT.BORDER | SWT.READ_ONLY); + wFormat.setItems( + new String[] { + SparkFileInputMeta.FORMAT_CSV, + SparkFileInputMeta.FORMAT_PARQUET, + SparkFileInputMeta.FORMAT_JSON, + SparkFileInputMeta.FORMAT_ORC, + SparkFileInputMeta.FORMAT_TEXT + }); + PropsUi.setLook(wFormat); + wFormat.addModifyListener(lsMod); + FormData fdFormat = new FormData(); + fdFormat.left = new FormAttachment(middle, 0); + fdFormat.top = new FormAttachment(wlFormat, 0, SWT.CENTER); + fdFormat.right = new FormAttachment(100, 0); + wFormat.setLayoutData(fdFormat); + last = wFormat; + + wHeader = new Button(shell, SWT.CHECK); + wHeader.setText(BaseMessages.getString(PKG, "SparkFileInputDialog.Header")); + PropsUi.setLook(wHeader); + FormData fdHeader = new FormData(); + fdHeader.left = new FormAttachment(middle, 0); + fdHeader.top = new FormAttachment(last, margin); + wHeader.setLayoutData(fdHeader); + last = wHeader; + + wSeparator = labeledTextVar(lsMod, middle, margin, last, "SparkFileInputDialog.Separator"); + last = wSeparator; + wQuote = labeledTextVar(lsMod, middle, margin, last, "SparkFileInputDialog.Quote"); + last = wQuote; + + wInferSchema = new Button(shell, SWT.CHECK); + wInferSchema.setText(BaseMessages.getString(PKG, "SparkFileInputDialog.InferSchema")); + PropsUi.setLook(wInferSchema); + FormData fdInfer = new FormData(); + fdInfer.left = new FormAttachment(middle, 0); + fdInfer.top = new FormAttachment(last, margin); + wInferSchema.setLayoutData(fdInfer); + last = wInferSchema; + + Label wlExtra = new Label(shell, SWT.RIGHT); + wlExtra.setText(BaseMessages.getString(PKG, "SparkFileInputDialog.ExtraOptions")); + PropsUi.setLook(wlExtra); + FormData fdlExtra = new FormData(); + fdlExtra.left = new FormAttachment(0, 0); + fdlExtra.top = new FormAttachment(last, margin); + fdlExtra.right = new FormAttachment(middle, -margin); + wlExtra.setLayoutData(fdlExtra); + wExtraOptions = new Text(shell, SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.V_SCROLL); + PropsUi.setLook(wExtraOptions); + wExtraOptions.addModifyListener(lsMod); + FormData fdExtra = new FormData(); + fdExtra.left = new FormAttachment(middle, 0); + fdExtra.top = new FormAttachment(wlExtra, 0, SWT.TOP); + fdExtra.right = new FormAttachment(100, 0); + fdExtra.height = 50; + wExtraOptions.setLayoutData(fdExtra); + last = wExtraOptions; + + // Fields table + Label wlFields = new Label(shell, SWT.NONE); + wlFields.setText(BaseMessages.getString(PKG, "SparkFileInputDialog.Fields")); + PropsUi.setLook(wlFields); + FormData fdlFields = new FormData(); + fdlFields.left = new FormAttachment(0, 0); + fdlFields.top = new FormAttachment(last, margin); + wlFields.setLayoutData(fdlFields); + + wOk = new Button(shell, SWT.PUSH); + wOk.setText(BaseMessages.getString(PKG, "System.Button.OK")); + wCancel = new Button(shell, SWT.PUSH); + wCancel.setText(BaseMessages.getString(PKG, "System.Button.Cancel")); + setButtonPositions(new Button[] {wOk, wCancel}, margin, null); + + ColumnInfo[] columns = + new ColumnInfo[] { + new ColumnInfo( + BaseMessages.getString(PKG, "SparkFileInputDialog.Column.Name"), + ColumnInfo.COLUMN_TYPE_TEXT, + false), + new ColumnInfo( + BaseMessages.getString(PKG, "SparkFileInputDialog.Column.Type"), + ColumnInfo.COLUMN_TYPE_CCOMBO, + ValueMetaFactory.getValueMetaNames()), + new ColumnInfo( + BaseMessages.getString(PKG, "SparkFileInputDialog.Column.Length"), + ColumnInfo.COLUMN_TYPE_TEXT, + false), + new ColumnInfo( + BaseMessages.getString(PKG, "SparkFileInputDialog.Column.Precision"), + ColumnInfo.COLUMN_TYPE_TEXT, + false), + }; + wFields = + new TableView( + variables, + shell, + SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI, + columns, + input.getFields() == null ? 1 : input.getFields().size(), + lsMod, + props); + FormData fdFields = new FormData(); + fdFields.left = new FormAttachment(0, 0); + fdFields.top = new FormAttachment(wlFields, margin); + fdFields.right = new FormAttachment(100, 0); + fdFields.bottom = new FormAttachment(wOk, -2 * margin); + wFields.setLayoutData(fdFields); + + wOk.addListener(SWT.Selection, e -> ok()); + wCancel.addListener(SWT.Selection, e -> cancel()); + + getData(); + input.setChanged(changed); + BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel()); + return transformName; + } + + private TextVar labeledTextVar( + ModifyListener lsMod, int middle, int margin, Control last, String labelKey) { + Label wl = new Label(shell, SWT.RIGHT); + wl.setText(BaseMessages.getString(PKG, labelKey)); + PropsUi.setLook(wl); + FormData fdl = new FormData(); + fdl.left = new FormAttachment(0, 0); + fdl.top = new FormAttachment(last, margin); + fdl.right = new FormAttachment(middle, -margin); + wl.setLayoutData(fdl); + TextVar text = new TextVar(variables, shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(text); + text.addModifyListener(lsMod); + String tooltip = BaseMessages.getString(PKG, labelKey + ".Tooltip"); + if (!Utils.isEmpty(tooltip) && !tooltip.startsWith("!")) { + text.setToolTipText(tooltip); + wl.setToolTipText(tooltip); + } + FormData fd = new FormData(); + fd.left = new FormAttachment(middle, 0); + fd.top = new FormAttachment(wl, 0, SWT.CENTER); + fd.right = new FormAttachment(100, 0); + text.setLayoutData(fd); + return text; + } + + private void getData() { + wTransformName.setText(Const.NVL(transformName, "")); + wFilePath.setText(Const.NVL(input.getFilePath(), "")); + wFormat.setText(Const.NVL(input.getFileFormat(), SparkFileInputMeta.FORMAT_CSV)); + wHeader.setSelection(input.isHeader()); + wSeparator.setText(Const.NVL(input.getSeparator(), ",")); + wQuote.setText(Const.NVL(input.getQuote(), "\"")); + wInferSchema.setSelection(input.isInferSchema()); + wExtraOptions.setText(Const.NVL(input.getExtraOptions(), "")); + if (input.getFields() != null) { + for (int i = 0; i < input.getFields().size(); i++) { + SparkField f = input.getFields().get(i); + TableItem item = wFields.table.getItem(i); + item.setText(1, Const.NVL(f.getName(), "")); + item.setText(2, Const.NVL(f.getHopType(), "String")); + item.setText(3, f.getLength() >= 0 ? Integer.toString(f.getLength()) : ""); + item.setText(4, f.getPrecision() >= 0 ? Integer.toString(f.getPrecision()) : ""); + } + } + wFields.setRowNums(); + wFields.optWidth(true); + wTransformName.selectAll(); + wTransformName.setFocus(); + } + + private void cancel() { + transformName = null; + input.setChanged(changed); + dispose(); + } + + private void ok() { + if (Utils.isEmpty(wTransformName.getText())) { + return; + } + transformName = wTransformName.getText(); + input.setFilePath(wFilePath.getText()); + input.setFileFormat(wFormat.getText()); + input.setHeader(wHeader.getSelection()); + input.setSeparator(wSeparator.getText()); + input.setQuote(wQuote.getText()); + input.setInferSchema(wInferSchema.getSelection()); + input.setExtraOptions(wExtraOptions.getText()); + List fields = new ArrayList<>(); + for (int i = 0; i < wFields.nrNonEmpty(); i++) { + TableItem item = wFields.getNonEmpty(i); + SparkField f = new SparkField(); + f.setName(item.getText(1)); + f.setHopType(item.getText(2)); + f.setLength(Const.toInt(item.getText(3), -1)); + f.setPrecision(Const.toInt(item.getText(4), -1)); + fields.add(f); + } + input.setFields(fields); + input.setChanged(); + dispose(); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileInputMeta.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileInputMeta.java new file mode 100644 index 00000000000..b79e679ef8d --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileInputMeta.java @@ -0,0 +1,127 @@ +/* + * 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.hop.spark.transforms.io; + +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import org.apache.hop.core.annotations.Transform; +import org.apache.hop.core.exception.HopPluginException; +import org.apache.hop.core.exception.HopTransformException; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.metadata.api.HopMetadataProperty; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.transform.BaseTransformMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.util.SparkConst; + +@Transform( + id = SparkConst.SPARK_FILE_INPUT_PLUGIN_ID, + name = "i18n::SparkFileInput.Name", + description = "i18n::SparkFileInput.Description", + image = "spark-file-input.svg", + categoryDescription = "i18n:org.apache.hop.pipeline.transform:BaseTransform.Category.BigData", + keywords = "i18n::SparkFileInput.Keyword", + documentationUrl = "/pipeline/transforms/spark-file-input.html", + supportedEngines = {SparkConst.PLUGIN_ID}) +@Getter +@Setter +public class SparkFileInputMeta extends BaseTransformMeta { + + public static final String FORMAT_CSV = "csv"; + public static final String FORMAT_PARQUET = "parquet"; + public static final String FORMAT_JSON = "json"; + public static final String FORMAT_ORC = "orc"; + public static final String FORMAT_TEXT = "text"; + + @HopMetadataProperty(key = "file_path") + private String filePath; + + /** csv, parquet, json, orc, text */ + @HopMetadataProperty(key = "file_format") + private String fileFormat = FORMAT_CSV; + + @HopMetadataProperty(key = "header") + private boolean header = true; + + @HopMetadataProperty(key = "separator") + private String separator = ","; + + @HopMetadataProperty(key = "quote") + private String quote = "\""; + + @HopMetadataProperty(key = "infer_schema") + private boolean inferSchema = false; + + @HopMetadataProperty(key = "multi_line") + private boolean multiLine = false; + + /** Extra Spark read options as key=value lines */ + @HopMetadataProperty(key = "extra_options") + private String extraOptions; + + @HopMetadataProperty(groupKey = "fields", key = "field") + private List fields = new ArrayList<>(); + + public SparkFileInputMeta() { + super(); + } + + @Override + public String getDialogClassName() { + return SparkFileInputDialog.class.getName(); + } + + @Override + public void getFields( + IRowMeta inputRowMeta, + String name, + IRowMeta[] info, + TransformMeta nextTransform, + IVariables variables, + IHopMetadataProvider metadataProvider) + throws HopTransformException { + inputRowMeta.clear(); + if (fields == null || fields.isEmpty()) { + return; + } + try { + for (SparkField field : fields) { + if (field.getName() != null && !field.getName().isEmpty()) { + inputRowMeta.addValueMeta(field.createValueMeta()); + } + } + } catch (HopPluginException e) { + throw new HopTransformException("Unable to create row meta for Spark File Input", e); + } + } + + public IRowMeta createRowMeta() throws HopPluginException { + org.apache.hop.core.row.RowMeta rowMeta = new org.apache.hop.core.row.RowMeta(); + if (fields != null) { + for (SparkField field : fields) { + if (field.getName() != null && !field.getName().isEmpty()) { + rowMeta.addValueMeta(field.createValueMeta()); + } + } + } + return rowMeta; + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileOutput.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileOutput.java new file mode 100644 index 00000000000..8e9d39a8fa4 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileOutput.java @@ -0,0 +1,47 @@ +/* + * 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.hop.spark.transforms.io; + +import org.apache.hop.core.exception.HopException; +import org.apache.hop.pipeline.Pipeline; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.BaseTransform; +import org.apache.hop.pipeline.transform.TransformMeta; + +/** + * Metadata-only transform on Local engine. On the native Spark engine it is converted to a Spark + * file write. + */ +public class SparkFileOutput extends BaseTransform { + + public SparkFileOutput( + TransformMeta transformMeta, + SparkFileOutputMeta meta, + SparkFileOutputData data, + int copyNr, + PipelineMeta pipelineMeta, + Pipeline pipeline) { + super(transformMeta, meta, data, copyNr, pipelineMeta, pipeline); + } + + @Override + public boolean processRow() throws HopException { + setOutputDone(); + return false; + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileOutputData.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileOutputData.java new file mode 100644 index 00000000000..85924d1aa13 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileOutputData.java @@ -0,0 +1,27 @@ +/* + * 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.hop.spark.transforms.io; + +import org.apache.hop.pipeline.transform.BaseTransformData; +import org.apache.hop.pipeline.transform.ITransformData; + +public class SparkFileOutputData extends BaseTransformData implements ITransformData { + public SparkFileOutputData() { + super(); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileOutputDialog.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileOutputDialog.java new file mode 100644 index 00000000000..ce6e042ef2f --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileOutputDialog.java @@ -0,0 +1,272 @@ +/* + * 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.hop.spark.transforms.io; + +import org.apache.hop.core.Const; +import org.apache.hop.core.util.Utils; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.i18n.BaseMessages; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.ui.core.PropsUi; +import org.apache.hop.ui.core.dialog.BaseDialog; +import org.apache.hop.ui.core.widget.TextVar; +import org.apache.hop.ui.pipeline.transform.BaseTransformDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.CCombo; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +public class SparkFileOutputDialog extends BaseTransformDialog { + private static final Class PKG = SparkFileOutputMeta.class; + + private final SparkFileOutputMeta input; + + private TextVar wFilePath; + private CCombo wFormat; + private CCombo wSaveMode; + private Button wHeader; + private TextVar wSeparator; + private TextVar wQuote; + private TextVar wPartitionBy; + private TextVar wCoalesce; + private Text wExtraOptions; + + public SparkFileOutputDialog( + Shell parent, + IVariables variables, + SparkFileOutputMeta transformMeta, + PipelineMeta pipelineMeta) { + super(parent, variables, transformMeta, pipelineMeta); + this.input = transformMeta; + } + + @Override + public String open() { + Shell parent = getParent(); + shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.RESIZE | SWT.MIN | SWT.MAX); + PropsUi.setLook(shell); + setShellImage(shell, input); + + ModifyListener lsMod = e -> input.setChanged(); + changed = input.hasChanged(); + + FormLayout formLayout = new FormLayout(); + formLayout.marginWidth = PropsUi.getFormMargin(); + formLayout.marginHeight = PropsUi.getFormMargin(); + shell.setLayout(formLayout); + shell.setText(BaseMessages.getString(PKG, "SparkFileOutputDialog.Shell.Title")); + + int middle = props.getMiddlePct(); + int margin = PropsUi.getMargin(); + + wlTransformName = new Label(shell, SWT.RIGHT); + wlTransformName.setText(BaseMessages.getString(PKG, "System.Label.TransformName")); + PropsUi.setLook(wlTransformName); + fdlTransformName = new FormData(); + fdlTransformName.left = new FormAttachment(0, 0); + fdlTransformName.top = new FormAttachment(0, margin); + fdlTransformName.right = new FormAttachment(middle, -margin); + wlTransformName.setLayoutData(fdlTransformName); + wTransformName = new Text(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + wTransformName.setText(transformName); + PropsUi.setLook(wTransformName); + wTransformName.addModifyListener(lsMod); + fdTransformName = new FormData(); + fdTransformName.left = new FormAttachment(middle, 0); + fdTransformName.top = new FormAttachment(wlTransformName, 0, SWT.CENTER); + fdTransformName.right = new FormAttachment(100, 0); + wTransformName.setLayoutData(fdTransformName); + Control last = wTransformName; + + last = labeledTextVar(lsMod, middle, margin, last, "SparkFileOutputDialog.FilePath"); + wFilePath = (TextVar) last; + + Label wlFormat = new Label(shell, SWT.RIGHT); + wlFormat.setText(BaseMessages.getString(PKG, "SparkFileOutputDialog.Format")); + PropsUi.setLook(wlFormat); + FormData fdlFormat = new FormData(); + fdlFormat.left = new FormAttachment(0, 0); + fdlFormat.top = new FormAttachment(last, margin); + fdlFormat.right = new FormAttachment(middle, -margin); + wlFormat.setLayoutData(fdlFormat); + wFormat = new CCombo(shell, SWT.BORDER | SWT.READ_ONLY); + wFormat.setItems( + new String[] { + SparkFileInputMeta.FORMAT_CSV, + SparkFileInputMeta.FORMAT_PARQUET, + SparkFileInputMeta.FORMAT_JSON, + SparkFileInputMeta.FORMAT_ORC, + SparkFileInputMeta.FORMAT_TEXT + }); + PropsUi.setLook(wFormat); + wFormat.addModifyListener(lsMod); + FormData fdFormat = new FormData(); + fdFormat.left = new FormAttachment(middle, 0); + fdFormat.top = new FormAttachment(wlFormat, 0, SWT.CENTER); + fdFormat.right = new FormAttachment(100, 0); + wFormat.setLayoutData(fdFormat); + last = wFormat; + + Label wlMode = new Label(shell, SWT.RIGHT); + wlMode.setText(BaseMessages.getString(PKG, "SparkFileOutputDialog.SaveMode")); + PropsUi.setLook(wlMode); + FormData fdlMode = new FormData(); + fdlMode.left = new FormAttachment(0, 0); + fdlMode.top = new FormAttachment(last, margin); + fdlMode.right = new FormAttachment(middle, -margin); + wlMode.setLayoutData(fdlMode); + wSaveMode = new CCombo(shell, SWT.BORDER | SWT.READ_ONLY); + wSaveMode.setItems( + new String[] { + SparkFileOutputMeta.MODE_OVERWRITE, + SparkFileOutputMeta.MODE_APPEND, + SparkFileOutputMeta.MODE_IGNORE, + SparkFileOutputMeta.MODE_ERROR + }); + PropsUi.setLook(wSaveMode); + wSaveMode.addModifyListener(lsMod); + FormData fdMode = new FormData(); + fdMode.left = new FormAttachment(middle, 0); + fdMode.top = new FormAttachment(wlMode, 0, SWT.CENTER); + fdMode.right = new FormAttachment(100, 0); + wSaveMode.setLayoutData(fdMode); + last = wSaveMode; + + wHeader = new Button(shell, SWT.CHECK); + wHeader.setText(BaseMessages.getString(PKG, "SparkFileOutputDialog.Header")); + PropsUi.setLook(wHeader); + FormData fdHeader = new FormData(); + fdHeader.left = new FormAttachment(middle, 0); + fdHeader.top = new FormAttachment(last, margin); + wHeader.setLayoutData(fdHeader); + last = wHeader; + + last = labeledTextVar(lsMod, middle, margin, last, "SparkFileOutputDialog.Separator"); + wSeparator = (TextVar) last; + last = labeledTextVar(lsMod, middle, margin, last, "SparkFileOutputDialog.Quote"); + wQuote = (TextVar) last; + last = labeledTextVar(lsMod, middle, margin, last, "SparkFileOutputDialog.PartitionBy"); + wPartitionBy = (TextVar) last; + last = labeledTextVar(lsMod, middle, margin, last, "SparkFileOutputDialog.Coalesce"); + wCoalesce = (TextVar) last; + + Label wlExtra = new Label(shell, SWT.RIGHT); + wlExtra.setText(BaseMessages.getString(PKG, "SparkFileOutputDialog.ExtraOptions")); + PropsUi.setLook(wlExtra); + FormData fdlExtra = new FormData(); + fdlExtra.left = new FormAttachment(0, 0); + fdlExtra.top = new FormAttachment(last, margin); + fdlExtra.right = new FormAttachment(middle, -margin); + wlExtra.setLayoutData(fdlExtra); + wExtraOptions = new Text(shell, SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.V_SCROLL); + PropsUi.setLook(wExtraOptions); + wExtraOptions.addModifyListener(lsMod); + FormData fdExtra = new FormData(); + fdExtra.left = new FormAttachment(middle, 0); + fdExtra.top = new FormAttachment(wlExtra, 0, SWT.TOP); + fdExtra.right = new FormAttachment(100, 0); + fdExtra.height = 60; + wExtraOptions.setLayoutData(fdExtra); + + wOk = new Button(shell, SWT.PUSH); + wOk.setText(BaseMessages.getString(PKG, "System.Button.OK")); + wCancel = new Button(shell, SWT.PUSH); + wCancel.setText(BaseMessages.getString(PKG, "System.Button.Cancel")); + setButtonPositions(new Button[] {wOk, wCancel}, margin, wExtraOptions); + + wOk.addListener(SWT.Selection, e -> ok()); + wCancel.addListener(SWT.Selection, e -> cancel()); + + getData(); + input.setChanged(changed); + BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel()); + return transformName; + } + + private TextVar labeledTextVar( + ModifyListener lsMod, int middle, int margin, Control last, String labelKey) { + Label wl = new Label(shell, SWT.RIGHT); + wl.setText(BaseMessages.getString(PKG, labelKey)); + PropsUi.setLook(wl); + FormData fdl = new FormData(); + fdl.left = new FormAttachment(0, 0); + fdl.top = new FormAttachment(last, margin); + fdl.right = new FormAttachment(middle, -margin); + wl.setLayoutData(fdl); + TextVar text = new TextVar(variables, shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(text); + text.addModifyListener(lsMod); + String tooltip = BaseMessages.getString(PKG, labelKey + ".Tooltip"); + if (!Utils.isEmpty(tooltip) && !tooltip.startsWith("!")) { + text.setToolTipText(tooltip); + wl.setToolTipText(tooltip); + } + FormData fd = new FormData(); + fd.left = new FormAttachment(middle, 0); + fd.top = new FormAttachment(wl, 0, SWT.CENTER); + fd.right = new FormAttachment(100, 0); + text.setLayoutData(fd); + return text; + } + + private void getData() { + wTransformName.setText(Const.NVL(transformName, "")); + wFilePath.setText(Const.NVL(input.getFilePath(), "")); + wFormat.setText(Const.NVL(input.getFileFormat(), SparkFileInputMeta.FORMAT_CSV)); + wSaveMode.setText(Const.NVL(input.getSaveMode(), SparkFileOutputMeta.MODE_OVERWRITE)); + wHeader.setSelection(input.isHeader()); + wSeparator.setText(Const.NVL(input.getSeparator(), ",")); + wQuote.setText(Const.NVL(input.getQuote(), "\"")); + wPartitionBy.setText(Const.NVL(input.getPartitionByColumns(), "")); + wCoalesce.setText(Const.NVL(input.getCoalescePartitions(), "")); + wExtraOptions.setText(Const.NVL(input.getExtraOptions(), "")); + wTransformName.selectAll(); + wTransformName.setFocus(); + } + + private void cancel() { + transformName = null; + input.setChanged(changed); + dispose(); + } + + private void ok() { + if (Utils.isEmpty(wTransformName.getText())) { + return; + } + transformName = wTransformName.getText(); + input.setFilePath(wFilePath.getText()); + input.setFileFormat(wFormat.getText()); + input.setSaveMode(wSaveMode.getText()); + input.setHeader(wHeader.getSelection()); + input.setSeparator(wSeparator.getText()); + input.setQuote(wQuote.getText()); + input.setPartitionByColumns(wPartitionBy.getText()); + input.setCoalescePartitions(wCoalesce.getText()); + input.setExtraOptions(wExtraOptions.getText()); + input.setChanged(); + dispose(); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileOutputMeta.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileOutputMeta.java new file mode 100644 index 00000000000..f9f768bb3de --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/io/SparkFileOutputMeta.java @@ -0,0 +1,99 @@ +/* + * 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.hop.spark.transforms.io; + +import lombok.Getter; +import lombok.Setter; +import org.apache.hop.core.annotations.Transform; +import org.apache.hop.core.exception.HopTransformException; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.metadata.api.HopMetadataProperty; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.transform.BaseTransformMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.util.SparkConst; + +@Transform( + id = SparkConst.SPARK_FILE_OUTPUT_PLUGIN_ID, + name = "i18n::SparkFileOutput.Name", + description = "i18n::SparkFileOutput.Description", + image = "spark-file-output.svg", + categoryDescription = "i18n:org.apache.hop.pipeline.transform:BaseTransform.Category.BigData", + keywords = "i18n::SparkFileOutput.Keyword", + documentationUrl = "/pipeline/transforms/spark-file-output.html", + supportedEngines = {SparkConst.PLUGIN_ID}) +@Getter +@Setter +public class SparkFileOutputMeta extends BaseTransformMeta { + + public static final String MODE_OVERWRITE = "Overwrite"; + public static final String MODE_APPEND = "Append"; + public static final String MODE_IGNORE = "Ignore"; + public static final String MODE_ERROR = "ErrorIfExists"; + + @HopMetadataProperty(key = "file_path") + private String filePath; + + /** csv, parquet, json, orc, text */ + @HopMetadataProperty(key = "file_format") + private String fileFormat = SparkFileInputMeta.FORMAT_CSV; + + @HopMetadataProperty(key = "save_mode") + private String saveMode = MODE_OVERWRITE; + + @HopMetadataProperty(key = "header") + private boolean header = true; + + @HopMetadataProperty(key = "separator") + private String separator = ","; + + @HopMetadataProperty(key = "quote") + private String quote = "\""; + + @HopMetadataProperty(key = "partition_by") + private String partitionByColumns; + + @HopMetadataProperty(key = "coalesce") + private String coalescePartitions; + + /** Extra Spark write options as key=value lines */ + @HopMetadataProperty(key = "extra_options") + private String extraOptions; + + public SparkFileOutputMeta() { + super(); + } + + @Override + public String getDialogClassName() { + return SparkFileOutputDialog.class.getName(); + } + + @Override + public void getFields( + IRowMeta inputRowMeta, + String name, + IRowMeta[] info, + TransformMeta nextTransform, + IVariables variables, + IHopMetadataProvider metadataProvider) + throws HopTransformException { + // Pass-through: output fields match input + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableInput.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableInput.java new file mode 100644 index 00000000000..741de475a1c --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableInput.java @@ -0,0 +1,47 @@ +/* + * 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.hop.spark.transforms.table; + +import org.apache.hop.core.exception.HopException; +import org.apache.hop.pipeline.Pipeline; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.BaseTransform; +import org.apache.hop.pipeline.transform.TransformMeta; + +/** + * Metadata-only on Local engine. On the native Spark engine this becomes a lake table Dataset read. + */ +public class SparkLakeTableInput + extends BaseTransform { + + public SparkLakeTableInput( + TransformMeta transformMeta, + SparkLakeTableInputMeta meta, + SparkLakeTableInputData data, + int copyNr, + PipelineMeta pipelineMeta, + Pipeline pipeline) { + super(transformMeta, meta, data, copyNr, pipelineMeta, pipeline); + } + + @Override + public boolean processRow() throws HopException { + setOutputDone(); + return false; + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableInputData.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableInputData.java new file mode 100644 index 00000000000..f89ee00e6ed --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableInputData.java @@ -0,0 +1,27 @@ +/* + * 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.hop.spark.transforms.table; + +import org.apache.hop.pipeline.transform.BaseTransformData; +import org.apache.hop.pipeline.transform.ITransformData; + +public class SparkLakeTableInputData extends BaseTransformData implements ITransformData { + public SparkLakeTableInputData() { + super(); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableInputDialog.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableInputDialog.java new file mode 100644 index 00000000000..d1ecbd73bdf --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableInputDialog.java @@ -0,0 +1,348 @@ +/* + * 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.hop.spark.transforms.table; + +import java.util.ArrayList; +import java.util.List; +import org.apache.hop.core.Const; +import org.apache.hop.core.row.value.ValueMetaFactory; +import org.apache.hop.core.util.Utils; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.i18n.BaseMessages; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.spark.table.SparkLakeFormats; +import org.apache.hop.spark.transforms.io.SparkField; +import org.apache.hop.ui.core.PropsUi; +import org.apache.hop.ui.core.dialog.BaseDialog; +import org.apache.hop.ui.core.widget.ColumnInfo; +import org.apache.hop.ui.core.widget.TableView; +import org.apache.hop.ui.core.widget.TextVar; +import org.apache.hop.ui.pipeline.transform.BaseTransformDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.CCombo; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.TableItem; +import org.eclipse.swt.widgets.Text; + +public class SparkLakeTableInputDialog extends BaseTransformDialog { + private static final Class PKG = SparkLakeTableInputMeta.class; + + private final SparkLakeTableInputMeta input; + + private CCombo wFormat; + private CCombo wIdentifierMode; + private TextVar wTablePath; + private TextVar wTableIdentifier; + private TextVar wCatalogMetadataName; + private CCombo wTimeTravelType; + private TextVar wTimeTravelVersion; + private TextVar wTimeTravelTimestamp; + private Text wExtraOptions; + private TableView wFields; + + public SparkLakeTableInputDialog( + Shell parent, + IVariables variables, + SparkLakeTableInputMeta transformMeta, + PipelineMeta pipelineMeta) { + super(parent, variables, transformMeta, pipelineMeta); + this.input = transformMeta; + } + + @Override + public String open() { + Shell parent = getParent(); + shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.RESIZE | SWT.MIN | SWT.MAX); + PropsUi.setLook(shell); + setShellImage(shell, input); + + ModifyListener lsMod = e -> input.setChanged(); + changed = input.hasChanged(); + + FormLayout formLayout = new FormLayout(); + formLayout.marginWidth = PropsUi.getFormMargin(); + formLayout.marginHeight = PropsUi.getFormMargin(); + shell.setLayout(formLayout); + shell.setText(BaseMessages.getString(PKG, "SparkLakeTableInputDialog.Shell.Title")); + + int middle = props.getMiddlePct(); + int margin = PropsUi.getMargin(); + + wlTransformName = new Label(shell, SWT.RIGHT); + wlTransformName.setText(BaseMessages.getString(PKG, "System.Label.TransformName")); + PropsUi.setLook(wlTransformName); + fdlTransformName = new FormData(); + fdlTransformName.left = new FormAttachment(0, 0); + fdlTransformName.top = new FormAttachment(0, margin); + fdlTransformName.right = new FormAttachment(middle, -margin); + wlTransformName.setLayoutData(fdlTransformName); + wTransformName = new Text(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + wTransformName.setText(transformName); + PropsUi.setLook(wTransformName); + wTransformName.addModifyListener(lsMod); + fdTransformName = new FormData(); + fdTransformName.left = new FormAttachment(middle, 0); + fdTransformName.top = new FormAttachment(wlTransformName, 0, SWT.CENTER); + fdTransformName.right = new FormAttachment(100, 0); + wTransformName.setLayoutData(fdTransformName); + Control last = wTransformName; + + last = labeledCombo(lsMod, middle, margin, last, "SparkLakeTableInputDialog.Format", true); + wFormat = (CCombo) last; + wFormat.setItems(new String[] {SparkLakeFormats.FORMAT_DELTA, SparkLakeFormats.FORMAT_ICEBERG}); + + last = + labeledCombo(lsMod, middle, margin, last, "SparkLakeTableInputDialog.IdentifierMode", true); + wIdentifierMode = (CCombo) last; + wIdentifierMode.setItems( + new String[] {SparkLakeTableInputMeta.MODE_PATH, SparkLakeTableInputMeta.MODE_TABLE}); + + last = labeledTextVar(lsMod, middle, margin, last, "SparkLakeTableInputDialog.TablePath"); + wTablePath = (TextVar) last; + + last = labeledTextVar(lsMod, middle, margin, last, "SparkLakeTableInputDialog.TableIdentifier"); + wTableIdentifier = (TextVar) last; + + last = + labeledTextVar( + lsMod, middle, margin, last, "SparkLakeTableInputDialog.CatalogMetadataName"); + wCatalogMetadataName = (TextVar) last; + + last = + labeledCombo(lsMod, middle, margin, last, "SparkLakeTableInputDialog.TimeTravelType", true); + wTimeTravelType = (CCombo) last; + wTimeTravelType.setItems( + new String[] { + SparkLakeTableInputMeta.TIME_TRAVEL_NONE, + SparkLakeTableInputMeta.TIME_TRAVEL_VERSION, + SparkLakeTableInputMeta.TIME_TRAVEL_TIMESTAMP + }); + + last = + labeledTextVar(lsMod, middle, margin, last, "SparkLakeTableInputDialog.TimeTravelVersion"); + wTimeTravelVersion = (TextVar) last; + + last = + labeledTextVar( + lsMod, middle, margin, last, "SparkLakeTableInputDialog.TimeTravelTimestamp"); + wTimeTravelTimestamp = (TextVar) last; + + Label wlExtra = new Label(shell, SWT.RIGHT); + wlExtra.setText(BaseMessages.getString(PKG, "SparkLakeTableInputDialog.ExtraOptions")); + PropsUi.setLook(wlExtra); + FormData fdlExtra = new FormData(); + fdlExtra.left = new FormAttachment(0, 0); + fdlExtra.top = new FormAttachment(last, margin); + fdlExtra.right = new FormAttachment(middle, -margin); + wlExtra.setLayoutData(fdlExtra); + wExtraOptions = new Text(shell, SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.V_SCROLL); + PropsUi.setLook(wExtraOptions); + wExtraOptions.addModifyListener(lsMod); + FormData fdExtra = new FormData(); + fdExtra.left = new FormAttachment(middle, 0); + fdExtra.top = new FormAttachment(wlExtra, 0, SWT.TOP); + fdExtra.right = new FormAttachment(100, 0); + fdExtra.height = 50; + wExtraOptions.setLayoutData(fdExtra); + last = wExtraOptions; + + Label wlFields = new Label(shell, SWT.RIGHT); + wlFields.setText(BaseMessages.getString(PKG, "SparkLakeTableInputDialog.Fields")); + PropsUi.setLook(wlFields); + FormData fdlFields = new FormData(); + fdlFields.left = new FormAttachment(0, 0); + fdlFields.top = new FormAttachment(last, margin); + fdlFields.right = new FormAttachment(middle, -margin); + wlFields.setLayoutData(fdlFields); + + ColumnInfo[] columns = + new ColumnInfo[] { + new ColumnInfo( + BaseMessages.getString(PKG, "SparkLakeTableInputDialog.Column.Name"), + ColumnInfo.COLUMN_TYPE_TEXT, + false), + new ColumnInfo( + BaseMessages.getString(PKG, "SparkLakeTableInputDialog.Column.Type"), + ColumnInfo.COLUMN_TYPE_CCOMBO, + ValueMetaFactory.getValueMetaNames()), + new ColumnInfo( + BaseMessages.getString(PKG, "SparkLakeTableInputDialog.Column.Length"), + ColumnInfo.COLUMN_TYPE_TEXT, + false), + new ColumnInfo( + BaseMessages.getString(PKG, "SparkLakeTableInputDialog.Column.Precision"), + ColumnInfo.COLUMN_TYPE_TEXT, + false) + }; + wFields = + new TableView( + variables, + shell, + SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI, + columns, + input.getFields() == null ? 1 : Math.max(1, input.getFields().size()), + lsMod, + props); + FormData fdFields = new FormData(); + fdFields.left = new FormAttachment(middle, 0); + fdFields.top = new FormAttachment(last, margin); + fdFields.right = new FormAttachment(100, 0); + fdFields.bottom = new FormAttachment(100, -50); + wFields.setLayoutData(fdFields); + + wOk = new Button(shell, SWT.PUSH); + wOk.setText(BaseMessages.getString(PKG, "System.Button.OK")); + wCancel = new Button(shell, SWT.PUSH); + wCancel.setText(BaseMessages.getString(PKG, "System.Button.Cancel")); + setButtonPositions(new Button[] {wOk, wCancel}, margin, null); + + wOk.addListener(SWT.Selection, e -> ok()); + wCancel.addListener(SWT.Selection, e -> cancel()); + + getData(); + input.setChanged(changed); + BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel()); + return transformName; + } + + private Control labeledCombo( + ModifyListener lsMod, + int middle, + int margin, + Control last, + String labelKey, + boolean readOnly) { + Label wl = new Label(shell, SWT.RIGHT); + wl.setText(BaseMessages.getString(PKG, labelKey)); + PropsUi.setLook(wl); + FormData fdl = new FormData(); + fdl.left = new FormAttachment(0, 0); + fdl.top = new FormAttachment(last, margin); + fdl.right = new FormAttachment(middle, -margin); + wl.setLayoutData(fdl); + CCombo combo = new CCombo(shell, SWT.BORDER | (readOnly ? SWT.READ_ONLY : SWT.NONE)); + PropsUi.setLook(combo); + combo.addModifyListener(lsMod); + FormData fd = new FormData(); + fd.left = new FormAttachment(middle, 0); + fd.top = new FormAttachment(wl, 0, SWT.CENTER); + fd.right = new FormAttachment(100, 0); + combo.setLayoutData(fd); + return combo; + } + + private TextVar labeledTextVar( + ModifyListener lsMod, int middle, int margin, Control last, String labelKey) { + Label wl = new Label(shell, SWT.RIGHT); + wl.setText(BaseMessages.getString(PKG, labelKey)); + PropsUi.setLook(wl); + FormData fdl = new FormData(); + fdl.left = new FormAttachment(0, 0); + fdl.top = new FormAttachment(last, margin); + fdl.right = new FormAttachment(middle, -margin); + wl.setLayoutData(fdl); + TextVar text = new TextVar(variables, shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(text); + text.addModifyListener(lsMod); + String tooltip = BaseMessages.getString(PKG, labelKey + ".Tooltip"); + if (!Utils.isEmpty(tooltip) && !tooltip.startsWith("!")) { + text.setToolTipText(tooltip); + wl.setToolTipText(tooltip); + } + FormData fd = new FormData(); + fd.left = new FormAttachment(middle, 0); + fd.top = new FormAttachment(wl, 0, SWT.CENTER); + fd.right = new FormAttachment(100, 0); + text.setLayoutData(fd); + return text; + } + + private void getData() { + wTransformName.setText(Const.NVL(transformName, "")); + wFormat.setText(Const.NVL(input.getFormat(), SparkLakeFormats.FORMAT_DELTA)); + wIdentifierMode.setText( + Const.NVL(input.getIdentifierMode(), SparkLakeTableInputMeta.MODE_PATH)); + wTablePath.setText(Const.NVL(input.getTablePath(), "")); + wTableIdentifier.setText(Const.NVL(input.getTableIdentifier(), "")); + wCatalogMetadataName.setText(Const.NVL(input.getCatalogMetadataName(), "")); + wTimeTravelType.setText( + Const.NVL(input.getTimeTravelType(), SparkLakeTableInputMeta.TIME_TRAVEL_NONE)); + wTimeTravelVersion.setText(Const.NVL(input.getTimeTravelVersion(), "")); + wTimeTravelTimestamp.setText(Const.NVL(input.getTimeTravelTimestamp(), "")); + wExtraOptions.setText(Const.NVL(input.getExtraOptions(), "")); + if (input.getFields() != null) { + int i = 0; + for (SparkField f : input.getFields()) { + TableItem item = wFields.table.getItem(i); + if (item == null) { + item = new TableItem(wFields.table, SWT.NONE); + } + item.setText(1, Const.NVL(f.getName(), "")); + item.setText(2, Const.NVL(f.getHopType(), "String")); + item.setText(3, f.getLength() >= 0 ? Integer.toString(f.getLength()) : ""); + item.setText(4, f.getPrecision() >= 0 ? Integer.toString(f.getPrecision()) : ""); + i++; + } + wFields.setRowNums(); + wFields.optWidth(true); + } + wTransformName.selectAll(); + wTransformName.setFocus(); + } + + private void cancel() { + transformName = null; + input.setChanged(changed); + dispose(); + } + + private void ok() { + if (Utils.isEmpty(wTransformName.getText())) { + return; + } + transformName = wTransformName.getText(); + input.setFormat(wFormat.getText()); + input.setIdentifierMode(wIdentifierMode.getText()); + input.setTablePath(wTablePath.getText()); + input.setTableIdentifier(wTableIdentifier.getText()); + input.setCatalogMetadataName(wCatalogMetadataName.getText()); + input.setTimeTravelType(wTimeTravelType.getText()); + input.setTimeTravelVersion(wTimeTravelVersion.getText()); + input.setTimeTravelTimestamp(wTimeTravelTimestamp.getText()); + input.setExtraOptions(wExtraOptions.getText()); + List fields = new ArrayList<>(); + for (int i = 0; i < wFields.nrNonEmpty(); i++) { + TableItem item = wFields.getNonEmpty(i); + SparkField f = new SparkField(); + f.setName(item.getText(1)); + f.setHopType(item.getText(2)); + f.setLength(Const.toInt(item.getText(3), -1)); + f.setPrecision(Const.toInt(item.getText(4), -1)); + fields.add(f); + } + input.setFields(fields); + dispose(); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableInputMeta.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableInputMeta.java new file mode 100644 index 00000000000..dfdb3d0a298 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableInputMeta.java @@ -0,0 +1,138 @@ +/* + * 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.hop.spark.transforms.table; + +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import org.apache.hop.core.annotations.Transform; +import org.apache.hop.core.exception.HopPluginException; +import org.apache.hop.core.exception.HopTransformException; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.metadata.api.HopMetadataProperty; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.transform.BaseTransformMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.table.SparkLakeFormats; +import org.apache.hop.spark.transforms.io.SparkField; +import org.apache.hop.spark.util.SparkConst; + +@Transform( + id = SparkConst.SPARK_LAKE_TABLE_INPUT_PLUGIN_ID, + name = "i18n::SparkLakeTableInput.Name", + description = "i18n::SparkLakeTableInput.Description", + image = "spark-lake-table-input.svg", + categoryDescription = "i18n:org.apache.hop.pipeline.transform:BaseTransform.Category.BigData", + keywords = "i18n::SparkLakeTableInput.Keyword", + documentationUrl = "/pipeline/transforms/spark-lake-table-input.html", + supportedEngines = {SparkConst.PLUGIN_ID}) +@Getter +@Setter +public class SparkLakeTableInputMeta + extends BaseTransformMeta { + + public static final String MODE_PATH = "PATH"; + public static final String MODE_TABLE = "TABLE"; + + public static final String TIME_TRAVEL_NONE = "NONE"; + public static final String TIME_TRAVEL_VERSION = "VERSION"; + public static final String TIME_TRAVEL_TIMESTAMP = "TIMESTAMP"; + + /** {@link SparkLakeFormats#FORMAT_DELTA} or {@link SparkLakeFormats#FORMAT_ICEBERG} */ + @HopMetadataProperty(key = "format", injectionKey = "FORMAT") + private String format = SparkLakeFormats.FORMAT_DELTA; + + /** PATH (v1 primary) or TABLE (catalog — later PRs) */ + @HopMetadataProperty(key = "identifier_mode", injectionKey = "IDENTIFIER_MODE") + private String identifierMode = MODE_PATH; + + @HopMetadataProperty(key = "table_path", injectionKey = "TABLE_PATH") + private String tablePath; + + /** Catalog-qualified table id when mode is TABLE (e.g. lake.db.orders). */ + @HopMetadataProperty(key = "table_identifier", injectionKey = "TABLE_IDENTIFIER") + private String tableIdentifier; + + /** Hop SparkCatalog metadata name when mode is TABLE. */ + @HopMetadataProperty(key = "catalog_metadata_name", injectionKey = "CATALOG_METADATA_NAME") + private String catalogMetadataName; + + /** + * Time travel: {@link #TIME_TRAVEL_NONE}, {@link #TIME_TRAVEL_VERSION}, or {@link + * #TIME_TRAVEL_TIMESTAMP}. + */ + @HopMetadataProperty(key = "time_travel_type", injectionKey = "TIME_TRAVEL_TYPE") + private String timeTravelType = TIME_TRAVEL_NONE; + + /** Delta version number or Iceberg snapshot id (when type is VERSION). Supports variables. */ + @HopMetadataProperty(key = "time_travel_version", injectionKey = "TIME_TRAVEL_VERSION") + private String timeTravelVersion; + + /** + * Spark-parseable timestamp string (when type is TIMESTAMP), e.g. {@code 2024-01-15 10:30:00}. + */ + @HopMetadataProperty(key = "time_travel_timestamp", injectionKey = "TIME_TRAVEL_TIMESTAMP") + private String timeTravelTimestamp; + + /** Extra Spark read options as key=value lines */ + @HopMetadataProperty(key = "extra_options", injectionKey = "EXTRA_OPTIONS") + private String extraOptions; + + /** Optional projection (name-based select/cast). */ + @HopMetadataProperty( + groupKey = "fields", + key = "field", + injectionGroupKey = "FIELDS", + injectionGroupDescription = "SparkLakeTableInput.Injection.Group.Fields") + private List fields = new ArrayList<>(); + + public SparkLakeTableInputMeta() { + super(); + } + + @Override + public String getDialogClassName() { + return SparkLakeTableInputDialog.class.getName(); + } + + @Override + public void getFields( + IRowMeta inputRowMeta, + String name, + IRowMeta[] info, + TransformMeta nextTransform, + IVariables variables, + IHopMetadataProvider metadataProvider) + throws HopTransformException { + inputRowMeta.clear(); + if (fields == null || fields.isEmpty()) { + return; + } + try { + for (SparkField field : fields) { + if (field.getName() != null && !field.getName().isEmpty()) { + inputRowMeta.addValueMeta(field.createValueMeta()); + } + } + } catch (HopPluginException e) { + throw new HopTransformException("Unable to create row meta for Spark Lake Table Input", e); + } + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMaintenance.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMaintenance.java new file mode 100644 index 00000000000..894de666d32 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMaintenance.java @@ -0,0 +1,48 @@ +/* + * 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.hop.spark.transforms.table; + +import org.apache.hop.core.exception.HopException; +import org.apache.hop.pipeline.Pipeline; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.BaseTransform; +import org.apache.hop.pipeline.transform.TransformMeta; + +/** + * Metadata-only on Local engine. On native Spark this runs OPTIMIZE / VACUUM / expire / DELETE as a + * zero-input action sink. + */ +public class SparkLakeTableMaintenance + extends BaseTransform { + + public SparkLakeTableMaintenance( + TransformMeta transformMeta, + SparkLakeTableMaintenanceMeta meta, + SparkLakeTableMaintenanceData data, + int copyNr, + PipelineMeta pipelineMeta, + Pipeline pipeline) { + super(transformMeta, meta, data, copyNr, pipelineMeta, pipeline); + } + + @Override + public boolean processRow() throws HopException { + setOutputDone(); + return false; + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMaintenanceData.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMaintenanceData.java new file mode 100644 index 00000000000..35bcb120374 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMaintenanceData.java @@ -0,0 +1,27 @@ +/* + * 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.hop.spark.transforms.table; + +import org.apache.hop.pipeline.transform.BaseTransformData; +import org.apache.hop.pipeline.transform.ITransformData; + +public class SparkLakeTableMaintenanceData extends BaseTransformData implements ITransformData { + public SparkLakeTableMaintenanceData() { + super(); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMaintenanceDialog.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMaintenanceDialog.java new file mode 100644 index 00000000000..414ac7194e1 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMaintenanceDialog.java @@ -0,0 +1,273 @@ +/* + * 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.hop.spark.transforms.table; + +import org.apache.hop.core.Const; +import org.apache.hop.core.util.Utils; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.i18n.BaseMessages; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.spark.table.SparkLakeFormats; +import org.apache.hop.spark.table.SparkMaintenanceSqlBuilder; +import org.apache.hop.ui.core.PropsUi; +import org.apache.hop.ui.core.dialog.BaseDialog; +import org.apache.hop.ui.core.widget.TextVar; +import org.apache.hop.ui.pipeline.transform.BaseTransformDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.CCombo; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +public class SparkLakeTableMaintenanceDialog extends BaseTransformDialog { + private static final Class PKG = SparkLakeTableMaintenanceMeta.class; + + private final SparkLakeTableMaintenanceMeta input; + + private CCombo wFormat; + private CCombo wIdentifierMode; + private TextVar wTablePath; + private TextVar wTableIdentifier; + private TextVar wCatalogMetadataName; + private CCombo wOperation; + private TextVar wRetentionHours; + private TextVar wRetainLast; + private TextVar wWhereClause; + private TextVar wZOrderColumns; + private Button wAcknowledgeDestructive; + + public SparkLakeTableMaintenanceDialog( + Shell parent, + IVariables variables, + SparkLakeTableMaintenanceMeta transformMeta, + PipelineMeta pipelineMeta) { + super(parent, variables, transformMeta, pipelineMeta); + this.input = transformMeta; + } + + @Override + public String open() { + Shell parent = getParent(); + shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.RESIZE | SWT.MIN | SWT.MAX); + PropsUi.setLook(shell); + setShellImage(shell, input); + + ModifyListener lsMod = e -> input.setChanged(); + changed = input.hasChanged(); + + FormLayout formLayout = new FormLayout(); + formLayout.marginWidth = PropsUi.getFormMargin(); + formLayout.marginHeight = PropsUi.getFormMargin(); + shell.setLayout(formLayout); + shell.setText(BaseMessages.getString(PKG, "SparkLakeTableMaintenanceDialog.Shell.Title")); + + int middle = props.getMiddlePct(); + int margin = PropsUi.getMargin(); + + wlTransformName = new Label(shell, SWT.RIGHT); + wlTransformName.setText(BaseMessages.getString(PKG, "System.Label.TransformName")); + PropsUi.setLook(wlTransformName); + fdlTransformName = new FormData(); + fdlTransformName.left = new FormAttachment(0, 0); + fdlTransformName.top = new FormAttachment(0, margin); + fdlTransformName.right = new FormAttachment(middle, -margin); + wlTransformName.setLayoutData(fdlTransformName); + wTransformName = new Text(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + wTransformName.setText(transformName); + PropsUi.setLook(wTransformName); + wTransformName.addModifyListener(lsMod); + fdTransformName = new FormData(); + fdTransformName.left = new FormAttachment(middle, 0); + fdTransformName.top = new FormAttachment(wlTransformName, 0, SWT.CENTER); + fdTransformName.right = new FormAttachment(100, 0); + wTransformName.setLayoutData(fdTransformName); + Control last = wTransformName; + + last = labeledCombo(lsMod, middle, margin, last, "SparkLakeTableMaintenanceDialog.Format"); + wFormat = (CCombo) last; + wFormat.setItems(new String[] {SparkLakeFormats.FORMAT_DELTA, SparkLakeFormats.FORMAT_ICEBERG}); + + last = + labeledCombo(lsMod, middle, margin, last, "SparkLakeTableMaintenanceDialog.IdentifierMode"); + wIdentifierMode = (CCombo) last; + wIdentifierMode.setItems( + new String[] {SparkLakeTableInputMeta.MODE_PATH, SparkLakeTableInputMeta.MODE_TABLE}); + + last = labeledTextVar(lsMod, middle, margin, last, "SparkLakeTableMaintenanceDialog.TablePath"); + wTablePath = (TextVar) last; + + last = + labeledTextVar( + lsMod, middle, margin, last, "SparkLakeTableMaintenanceDialog.TableIdentifier"); + wTableIdentifier = (TextVar) last; + + last = + labeledTextVar( + lsMod, middle, margin, last, "SparkLakeTableMaintenanceDialog.CatalogMetadataName"); + wCatalogMetadataName = (TextVar) last; + + last = labeledCombo(lsMod, middle, margin, last, "SparkLakeTableMaintenanceDialog.Operation"); + wOperation = (CCombo) last; + wOperation.setItems( + new String[] { + SparkMaintenanceSqlBuilder.OP_OPTIMIZE, + SparkMaintenanceSqlBuilder.OP_VACUUM, + SparkMaintenanceSqlBuilder.OP_EXPIRE_SNAPSHOTS, + SparkMaintenanceSqlBuilder.OP_REWRITE_MANIFESTS, + SparkMaintenanceSqlBuilder.OP_DELETE_WHERE + }); + + last = + labeledTextVar( + lsMod, middle, margin, last, "SparkLakeTableMaintenanceDialog.RetentionHours"); + wRetentionHours = (TextVar) last; + + last = + labeledTextVar(lsMod, middle, margin, last, "SparkLakeTableMaintenanceDialog.RetainLast"); + wRetainLast = (TextVar) last; + + last = + labeledTextVar(lsMod, middle, margin, last, "SparkLakeTableMaintenanceDialog.WhereClause"); + wWhereClause = (TextVar) last; + + last = + labeledTextVar( + lsMod, middle, margin, last, "SparkLakeTableMaintenanceDialog.ZOrderColumns"); + wZOrderColumns = (TextVar) last; + + wAcknowledgeDestructive = new Button(shell, SWT.CHECK); + wAcknowledgeDestructive.setText( + BaseMessages.getString(PKG, "SparkLakeTableMaintenanceDialog.AcknowledgeDestructive")); + PropsUi.setLook(wAcknowledgeDestructive); + FormData fdAck = new FormData(); + fdAck.left = new FormAttachment(middle, 0); + fdAck.top = new FormAttachment(last, margin); + wAcknowledgeDestructive.setLayoutData(fdAck); + wAcknowledgeDestructive.addListener(SWT.Selection, e -> input.setChanged()); + + wOk = new Button(shell, SWT.PUSH); + wOk.setText(BaseMessages.getString(PKG, "System.Button.OK")); + wCancel = new Button(shell, SWT.PUSH); + wCancel.setText(BaseMessages.getString(PKG, "System.Button.Cancel")); + setButtonPositions(new Button[] {wOk, wCancel}, margin, wAcknowledgeDestructive); + + wOk.addListener(SWT.Selection, e -> ok()); + wCancel.addListener(SWT.Selection, e -> cancel()); + + getData(); + input.setChanged(changed); + BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel()); + return transformName; + } + + private Control labeledCombo( + ModifyListener lsMod, int middle, int margin, Control last, String labelKey) { + Label wl = new Label(shell, SWT.RIGHT); + wl.setText(BaseMessages.getString(PKG, labelKey)); + PropsUi.setLook(wl); + FormData fdl = new FormData(); + fdl.left = new FormAttachment(0, 0); + fdl.top = new FormAttachment(last, margin); + fdl.right = new FormAttachment(middle, -margin); + wl.setLayoutData(fdl); + CCombo combo = new CCombo(shell, SWT.BORDER | SWT.READ_ONLY); + PropsUi.setLook(combo); + combo.addModifyListener(lsMod); + FormData fd = new FormData(); + fd.left = new FormAttachment(middle, 0); + fd.top = new FormAttachment(wl, 0, SWT.CENTER); + fd.right = new FormAttachment(100, 0); + combo.setLayoutData(fd); + return combo; + } + + private TextVar labeledTextVar( + ModifyListener lsMod, int middle, int margin, Control last, String labelKey) { + Label wl = new Label(shell, SWT.RIGHT); + wl.setText(BaseMessages.getString(PKG, labelKey)); + PropsUi.setLook(wl); + FormData fdl = new FormData(); + fdl.left = new FormAttachment(0, 0); + fdl.top = new FormAttachment(last, margin); + fdl.right = new FormAttachment(middle, -margin); + wl.setLayoutData(fdl); + TextVar text = new TextVar(variables, shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(text); + text.addModifyListener(lsMod); + String tooltip = BaseMessages.getString(PKG, labelKey + ".Tooltip"); + if (!Utils.isEmpty(tooltip) && !tooltip.startsWith("!")) { + text.setToolTipText(tooltip); + wl.setToolTipText(tooltip); + } + FormData fd = new FormData(); + fd.left = new FormAttachment(middle, 0); + fd.top = new FormAttachment(wl, 0, SWT.CENTER); + fd.right = new FormAttachment(100, 0); + text.setLayoutData(fd); + return text; + } + + private void getData() { + wTransformName.setText(Const.NVL(transformName, "")); + wFormat.setText(Const.NVL(input.getFormat(), SparkLakeFormats.FORMAT_DELTA)); + wIdentifierMode.setText( + Const.NVL(input.getIdentifierMode(), SparkLakeTableInputMeta.MODE_PATH)); + wTablePath.setText(Const.NVL(input.getTablePath(), "")); + wTableIdentifier.setText(Const.NVL(input.getTableIdentifier(), "")); + wCatalogMetadataName.setText(Const.NVL(input.getCatalogMetadataName(), "")); + wOperation.setText(Const.NVL(input.getOperation(), SparkMaintenanceSqlBuilder.OP_OPTIMIZE)); + wRetentionHours.setText(Const.NVL(input.getRetentionHours(), "")); + wRetainLast.setText(Const.NVL(input.getRetainLast(), "1")); + wWhereClause.setText(Const.NVL(input.getWhereClause(), "")); + wZOrderColumns.setText(Const.NVL(input.getZOrderColumns(), "")); + wAcknowledgeDestructive.setSelection(input.isAcknowledgeDestructive()); + wTransformName.selectAll(); + wTransformName.setFocus(); + } + + private void cancel() { + transformName = null; + input.setChanged(changed); + dispose(); + } + + private void ok() { + if (Utils.isEmpty(wTransformName.getText())) { + return; + } + transformName = wTransformName.getText(); + input.setFormat(wFormat.getText()); + input.setIdentifierMode(wIdentifierMode.getText()); + input.setTablePath(wTablePath.getText()); + input.setTableIdentifier(wTableIdentifier.getText()); + input.setCatalogMetadataName(wCatalogMetadataName.getText()); + input.setOperation(wOperation.getText()); + input.setRetentionHours(wRetentionHours.getText()); + input.setRetainLast(wRetainLast.getText()); + input.setWhereClause(wWhereClause.getText()); + input.setZOrderColumns(wZOrderColumns.getText()); + input.setAcknowledgeDestructive(wAcknowledgeDestructive.getSelection()); + dispose(); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMaintenanceMeta.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMaintenanceMeta.java new file mode 100644 index 00000000000..e3632ec1977 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMaintenanceMeta.java @@ -0,0 +1,110 @@ +/* + * 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.hop.spark.transforms.table; + +import lombok.Getter; +import lombok.Setter; +import org.apache.hop.core.annotations.Transform; +import org.apache.hop.core.exception.HopTransformException; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.metadata.api.HopMetadataProperty; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.transform.BaseTransformMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.table.SparkLakeFormats; +import org.apache.hop.spark.table.SparkMaintenanceSqlBuilder; +import org.apache.hop.spark.util.SparkConst; + +@Transform( + id = SparkConst.SPARK_LAKE_TABLE_MAINTENANCE_PLUGIN_ID, + name = "i18n::SparkLakeTableMaintenance.Name", + description = "i18n::SparkLakeTableMaintenance.Description", + image = "spark-lake-table-maintenance.svg", + categoryDescription = "i18n:org.apache.hop.pipeline.transform:BaseTransform.Category.BigData", + keywords = "i18n::SparkLakeTableMaintenance.Keyword", + documentationUrl = "/pipeline/transforms/spark-lake-table-maintenance.html", + supportedEngines = {SparkConst.PLUGIN_ID}) +@Getter +@Setter +public class SparkLakeTableMaintenanceMeta + extends BaseTransformMeta { + + @HopMetadataProperty(key = "format", injectionKey = "FORMAT") + private String format = SparkLakeFormats.FORMAT_DELTA; + + @HopMetadataProperty(key = "identifier_mode", injectionKey = "IDENTIFIER_MODE") + private String identifierMode = SparkLakeTableInputMeta.MODE_PATH; + + @HopMetadataProperty(key = "table_path", injectionKey = "TABLE_PATH") + private String tablePath; + + @HopMetadataProperty(key = "table_identifier", injectionKey = "TABLE_IDENTIFIER") + private String tableIdentifier; + + @HopMetadataProperty(key = "catalog_metadata_name", injectionKey = "CATALOG_METADATA_NAME") + private String catalogMetadataName; + + /** {@link SparkMaintenanceSqlBuilder} operation constants. */ + @HopMetadataProperty(key = "operation", injectionKey = "OPERATION") + private String operation = SparkMaintenanceSqlBuilder.OP_OPTIMIZE; + + /** Required for VACUUM / EXPIRE_SNAPSHOTS (hours). No silent default. */ + @HopMetadataProperty(key = "retention_hours", injectionKey = "RETENTION_HOURS") + private String retentionHours; + + /** Iceberg expire_snapshots retain_last (default 1 when retentionHours set). */ + @HopMetadataProperty(key = "retain_last", injectionKey = "RETAIN_LAST") + private String retainLast; + + /** Optional WHERE for OPTIMIZE / DELETE_WHERE. */ + @HopMetadataProperty(key = "where_clause", injectionKey = "WHERE_CLAUSE") + private String whereClause; + + /** Optional Delta OPTIMIZE ZORDER BY columns (comma-separated). */ + @HopMetadataProperty(key = "zorder_columns", injectionKey = "ZORDER_COLUMNS") + private String zOrderColumns; + + /** + * Must be true for destructive ops (VACUUM, EXPIRE_SNAPSHOTS, DELETE_WHERE). Operator + * acknowledgement that data files / snapshots may be removed. + */ + @HopMetadataProperty(key = "acknowledge_destructive", injectionKey = "ACKNOWLEDGE_DESTRUCTIVE") + private boolean acknowledgeDestructive; + + public SparkLakeTableMaintenanceMeta() { + super(); + } + + @Override + public String getDialogClassName() { + return SparkLakeTableMaintenanceDialog.class.getName(); + } + + @Override + public void getFields( + IRowMeta inputRowMeta, + String name, + IRowMeta[] info, + TransformMeta nextTransform, + IVariables variables, + IHopMetadataProvider metadataProvider) + throws HopTransformException { + // Zero-input action sink + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMerge.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMerge.java new file mode 100644 index 00000000000..5de744c18fb --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMerge.java @@ -0,0 +1,48 @@ +/* + * 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.hop.spark.transforms.table; + +import org.apache.hop.core.exception.HopException; +import org.apache.hop.pipeline.Pipeline; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.BaseTransform; +import org.apache.hop.pipeline.transform.TransformMeta; + +/** + * Metadata-only on Local engine. On native Spark this becomes a MERGE INTO action against a lake + * table. + */ +public class SparkLakeTableMerge + extends BaseTransform { + + public SparkLakeTableMerge( + TransformMeta transformMeta, + SparkLakeTableMergeMeta meta, + SparkLakeTableMergeData data, + int copyNr, + PipelineMeta pipelineMeta, + Pipeline pipeline) { + super(transformMeta, meta, data, copyNr, pipelineMeta, pipeline); + } + + @Override + public boolean processRow() throws HopException { + setOutputDone(); + return false; + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMergeData.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMergeData.java new file mode 100644 index 00000000000..f8c59640965 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMergeData.java @@ -0,0 +1,27 @@ +/* + * 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.hop.spark.transforms.table; + +import org.apache.hop.pipeline.transform.BaseTransformData; +import org.apache.hop.pipeline.transform.ITransformData; + +public class SparkLakeTableMergeData extends BaseTransformData implements ITransformData { + public SparkLakeTableMergeData() { + super(); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMergeDialog.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMergeDialog.java new file mode 100644 index 00000000000..e3a6ff9e02b --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMergeDialog.java @@ -0,0 +1,279 @@ +/* + * 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.hop.spark.transforms.table; + +import org.apache.hop.core.Const; +import org.apache.hop.core.util.Utils; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.i18n.BaseMessages; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.spark.table.SparkLakeFormats; +import org.apache.hop.spark.table.SparkMergeSqlBuilder; +import org.apache.hop.ui.core.PropsUi; +import org.apache.hop.ui.core.dialog.BaseDialog; +import org.apache.hop.ui.core.widget.TextVar; +import org.apache.hop.ui.pipeline.transform.BaseTransformDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.CCombo; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +public class SparkLakeTableMergeDialog extends BaseTransformDialog { + private static final Class PKG = SparkLakeTableMergeMeta.class; + + private final SparkLakeTableMergeMeta input; + + private CCombo wFormat; + private CCombo wIdentifierMode; + private TextVar wTablePath; + private TextVar wTableIdentifier; + private TextVar wCatalogMetadataName; + private TextVar wMergeCondition; + private CCombo wMatchedAction; + private CCombo wNotMatchedAction; + private CCombo wNotMatchedBySourceAction; + private Text wRawMergeSql; + + public SparkLakeTableMergeDialog( + Shell parent, + IVariables variables, + SparkLakeTableMergeMeta transformMeta, + PipelineMeta pipelineMeta) { + super(parent, variables, transformMeta, pipelineMeta); + this.input = transformMeta; + } + + @Override + public String open() { + Shell parent = getParent(); + shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.RESIZE | SWT.MIN | SWT.MAX); + PropsUi.setLook(shell); + setShellImage(shell, input); + + ModifyListener lsMod = e -> input.setChanged(); + changed = input.hasChanged(); + + FormLayout formLayout = new FormLayout(); + formLayout.marginWidth = PropsUi.getFormMargin(); + formLayout.marginHeight = PropsUi.getFormMargin(); + shell.setLayout(formLayout); + shell.setText(BaseMessages.getString(PKG, "SparkLakeTableMergeDialog.Shell.Title")); + + int middle = props.getMiddlePct(); + int margin = PropsUi.getMargin(); + + wlTransformName = new Label(shell, SWT.RIGHT); + wlTransformName.setText(BaseMessages.getString(PKG, "System.Label.TransformName")); + PropsUi.setLook(wlTransformName); + fdlTransformName = new FormData(); + fdlTransformName.left = new FormAttachment(0, 0); + fdlTransformName.top = new FormAttachment(0, margin); + fdlTransformName.right = new FormAttachment(middle, -margin); + wlTransformName.setLayoutData(fdlTransformName); + wTransformName = new Text(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + wTransformName.setText(transformName); + PropsUi.setLook(wTransformName); + wTransformName.addModifyListener(lsMod); + fdTransformName = new FormData(); + fdTransformName.left = new FormAttachment(middle, 0); + fdTransformName.top = new FormAttachment(wlTransformName, 0, SWT.CENTER); + fdTransformName.right = new FormAttachment(100, 0); + wTransformName.setLayoutData(fdTransformName); + Control last = wTransformName; + + last = labeledCombo(lsMod, middle, margin, last, "SparkLakeTableMergeDialog.Format"); + wFormat = (CCombo) last; + wFormat.setItems(new String[] {SparkLakeFormats.FORMAT_DELTA, SparkLakeFormats.FORMAT_ICEBERG}); + + last = labeledCombo(lsMod, middle, margin, last, "SparkLakeTableMergeDialog.IdentifierMode"); + wIdentifierMode = (CCombo) last; + wIdentifierMode.setItems( + new String[] {SparkLakeTableInputMeta.MODE_PATH, SparkLakeTableInputMeta.MODE_TABLE}); + + last = labeledTextVar(lsMod, middle, margin, last, "SparkLakeTableMergeDialog.TablePath"); + wTablePath = (TextVar) last; + + last = labeledTextVar(lsMod, middle, margin, last, "SparkLakeTableMergeDialog.TableIdentifier"); + wTableIdentifier = (TextVar) last; + + last = + labeledTextVar( + lsMod, middle, margin, last, "SparkLakeTableMergeDialog.CatalogMetadataName"); + wCatalogMetadataName = (TextVar) last; + + last = labeledTextVar(lsMod, middle, margin, last, "SparkLakeTableMergeDialog.MergeCondition"); + wMergeCondition = (TextVar) last; + + last = labeledCombo(lsMod, middle, margin, last, "SparkLakeTableMergeDialog.MatchedAction"); + wMatchedAction = (CCombo) last; + wMatchedAction.setItems( + new String[] { + SparkMergeSqlBuilder.MATCHED_UPDATE_ALL, + SparkMergeSqlBuilder.MATCHED_DELETE, + SparkMergeSqlBuilder.MATCHED_NONE + }); + + last = labeledCombo(lsMod, middle, margin, last, "SparkLakeTableMergeDialog.NotMatchedAction"); + wNotMatchedAction = (CCombo) last; + wNotMatchedAction.setItems( + new String[] { + SparkMergeSqlBuilder.NOT_MATCHED_INSERT_ALL, SparkMergeSqlBuilder.NOT_MATCHED_NONE + }); + + last = + labeledCombo( + lsMod, middle, margin, last, "SparkLakeTableMergeDialog.NotMatchedBySourceAction"); + wNotMatchedBySourceAction = (CCombo) last; + wNotMatchedBySourceAction.setItems( + new String[] { + SparkMergeSqlBuilder.NOT_MATCHED_BY_SOURCE_NONE, + SparkMergeSqlBuilder.NOT_MATCHED_BY_SOURCE_DELETE + }); + + Label wlRaw = new Label(shell, SWT.RIGHT); + wlRaw.setText(BaseMessages.getString(PKG, "SparkLakeTableMergeDialog.RawMergeSql")); + PropsUi.setLook(wlRaw); + FormData fdlRaw = new FormData(); + fdlRaw.left = new FormAttachment(0, 0); + fdlRaw.top = new FormAttachment(last, margin); + fdlRaw.right = new FormAttachment(middle, -margin); + wlRaw.setLayoutData(fdlRaw); + wRawMergeSql = new Text(shell, SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.V_SCROLL); + PropsUi.setLook(wRawMergeSql); + wRawMergeSql.addModifyListener(lsMod); + FormData fdRaw = new FormData(); + fdRaw.left = new FormAttachment(middle, 0); + fdRaw.top = new FormAttachment(wlRaw, 0, SWT.TOP); + fdRaw.right = new FormAttachment(100, 0); + fdRaw.height = 80; + wRawMergeSql.setLayoutData(fdRaw); + + wOk = new Button(shell, SWT.PUSH); + wOk.setText(BaseMessages.getString(PKG, "System.Button.OK")); + wCancel = new Button(shell, SWT.PUSH); + wCancel.setText(BaseMessages.getString(PKG, "System.Button.Cancel")); + setButtonPositions(new Button[] {wOk, wCancel}, margin, wRawMergeSql); + + wOk.addListener(SWT.Selection, e -> ok()); + wCancel.addListener(SWT.Selection, e -> cancel()); + + getData(); + input.setChanged(changed); + BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel()); + return transformName; + } + + private Control labeledCombo( + ModifyListener lsMod, int middle, int margin, Control last, String labelKey) { + Label wl = new Label(shell, SWT.RIGHT); + wl.setText(BaseMessages.getString(PKG, labelKey)); + PropsUi.setLook(wl); + FormData fdl = new FormData(); + fdl.left = new FormAttachment(0, 0); + fdl.top = new FormAttachment(last, margin); + fdl.right = new FormAttachment(middle, -margin); + wl.setLayoutData(fdl); + CCombo combo = new CCombo(shell, SWT.BORDER | SWT.READ_ONLY); + PropsUi.setLook(combo); + combo.addModifyListener(lsMod); + FormData fd = new FormData(); + fd.left = new FormAttachment(middle, 0); + fd.top = new FormAttachment(wl, 0, SWT.CENTER); + fd.right = new FormAttachment(100, 0); + combo.setLayoutData(fd); + return combo; + } + + private TextVar labeledTextVar( + ModifyListener lsMod, int middle, int margin, Control last, String labelKey) { + Label wl = new Label(shell, SWT.RIGHT); + wl.setText(BaseMessages.getString(PKG, labelKey)); + PropsUi.setLook(wl); + FormData fdl = new FormData(); + fdl.left = new FormAttachment(0, 0); + fdl.top = new FormAttachment(last, margin); + fdl.right = new FormAttachment(middle, -margin); + wl.setLayoutData(fdl); + TextVar text = new TextVar(variables, shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(text); + text.addModifyListener(lsMod); + String tooltip = BaseMessages.getString(PKG, labelKey + ".Tooltip"); + if (!Utils.isEmpty(tooltip) && !tooltip.startsWith("!")) { + text.setToolTipText(tooltip); + wl.setToolTipText(tooltip); + } + FormData fd = new FormData(); + fd.left = new FormAttachment(middle, 0); + fd.top = new FormAttachment(wl, 0, SWT.CENTER); + fd.right = new FormAttachment(100, 0); + text.setLayoutData(fd); + return text; + } + + private void getData() { + wTransformName.setText(Const.NVL(transformName, "")); + wFormat.setText(Const.NVL(input.getFormat(), SparkLakeFormats.FORMAT_DELTA)); + wIdentifierMode.setText( + Const.NVL(input.getIdentifierMode(), SparkLakeTableInputMeta.MODE_PATH)); + wTablePath.setText(Const.NVL(input.getTablePath(), "")); + wTableIdentifier.setText(Const.NVL(input.getTableIdentifier(), "")); + wCatalogMetadataName.setText(Const.NVL(input.getCatalogMetadataName(), "")); + wMergeCondition.setText(Const.NVL(input.getMergeCondition(), "t.id = s.id")); + wMatchedAction.setText( + Const.NVL(input.getMatchedAction(), SparkMergeSqlBuilder.MATCHED_UPDATE_ALL)); + wNotMatchedAction.setText( + Const.NVL(input.getNotMatchedAction(), SparkMergeSqlBuilder.NOT_MATCHED_INSERT_ALL)); + wNotMatchedBySourceAction.setText( + Const.NVL( + input.getNotMatchedBySourceAction(), SparkMergeSqlBuilder.NOT_MATCHED_BY_SOURCE_NONE)); + wRawMergeSql.setText(Const.NVL(input.getRawMergeSql(), "")); + wTransformName.selectAll(); + wTransformName.setFocus(); + } + + private void cancel() { + transformName = null; + input.setChanged(changed); + dispose(); + } + + private void ok() { + if (Utils.isEmpty(wTransformName.getText())) { + return; + } + transformName = wTransformName.getText(); + input.setFormat(wFormat.getText()); + input.setIdentifierMode(wIdentifierMode.getText()); + input.setTablePath(wTablePath.getText()); + input.setTableIdentifier(wTableIdentifier.getText()); + input.setCatalogMetadataName(wCatalogMetadataName.getText()); + input.setMergeCondition(wMergeCondition.getText()); + input.setMatchedAction(wMatchedAction.getText()); + input.setNotMatchedAction(wNotMatchedAction.getText()); + input.setNotMatchedBySourceAction(wNotMatchedBySourceAction.getText()); + input.setRawMergeSql(wRawMergeSql.getText()); + dispose(); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMergeMeta.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMergeMeta.java new file mode 100644 index 00000000000..c2bbcd95da8 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableMergeMeta.java @@ -0,0 +1,111 @@ +/* + * 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.hop.spark.transforms.table; + +import lombok.Getter; +import lombok.Setter; +import org.apache.hop.core.annotations.Transform; +import org.apache.hop.core.exception.HopTransformException; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.metadata.api.HopMetadataProperty; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.transform.BaseTransformMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.table.SparkLakeFormats; +import org.apache.hop.spark.table.SparkMergeSqlBuilder; +import org.apache.hop.spark.util.SparkConst; + +@Transform( + id = SparkConst.SPARK_LAKE_TABLE_MERGE_PLUGIN_ID, + name = "i18n::SparkLakeTableMerge.Name", + description = "i18n::SparkLakeTableMerge.Description", + image = "spark-lake-table-merge.svg", + categoryDescription = "i18n:org.apache.hop.pipeline.transform:BaseTransform.Category.BigData", + keywords = "i18n::SparkLakeTableMerge.Keyword", + documentationUrl = "/pipeline/transforms/spark-lake-table-merge.html", + supportedEngines = {SparkConst.PLUGIN_ID}) +@Getter +@Setter +public class SparkLakeTableMergeMeta + extends BaseTransformMeta { + + @HopMetadataProperty(key = "format", injectionKey = "FORMAT") + private String format = SparkLakeFormats.FORMAT_DELTA; + + @HopMetadataProperty(key = "identifier_mode", injectionKey = "IDENTIFIER_MODE") + private String identifierMode = SparkLakeTableInputMeta.MODE_PATH; + + @HopMetadataProperty(key = "table_path", injectionKey = "TABLE_PATH") + private String tablePath; + + @HopMetadataProperty(key = "table_identifier", injectionKey = "TABLE_IDENTIFIER") + private String tableIdentifier; + + @HopMetadataProperty(key = "catalog_metadata_name", injectionKey = "CATALOG_METADATA_NAME") + private String catalogMetadataName; + + /** ON clause, e.g. {@code t.id = s.id} (t = target, s = source). */ + @HopMetadataProperty(key = "merge_condition", injectionKey = "MERGE_CONDITION") + private String mergeCondition; + + /** {@link SparkMergeSqlBuilder#MATCHED_UPDATE_ALL}, DELETE, or NONE */ + @HopMetadataProperty(key = "matched_action", injectionKey = "MATCHED_ACTION") + private String matchedAction = SparkMergeSqlBuilder.MATCHED_UPDATE_ALL; + + /** {@link SparkMergeSqlBuilder#NOT_MATCHED_INSERT_ALL} or NONE */ + @HopMetadataProperty(key = "not_matched_action", injectionKey = "NOT_MATCHED_ACTION") + private String notMatchedAction = SparkMergeSqlBuilder.NOT_MATCHED_INSERT_ALL; + + /** + * Optional {@link SparkMergeSqlBuilder#NOT_MATCHED_BY_SOURCE_DELETE} (Delta; Iceberg support + * varies). Default NONE. + */ + @HopMetadataProperty( + key = "not_matched_by_source_action", + injectionKey = "NOT_MATCHED_BY_SOURCE_ACTION") + private String notMatchedBySourceAction = SparkMergeSqlBuilder.NOT_MATCHED_BY_SOURCE_NONE; + + /** + * Advanced: full MERGE SQL. When non-empty, overrides structured fields. Operator is trusted; + * empty by default. + */ + @HopMetadataProperty(key = "raw_merge_sql", injectionKey = "RAW_MERGE_SQL") + private String rawMergeSql; + + public SparkLakeTableMergeMeta() { + super(); + } + + @Override + public String getDialogClassName() { + return SparkLakeTableMergeDialog.class.getName(); + } + + @Override + public void getFields( + IRowMeta inputRowMeta, + String name, + IRowMeta[] info, + TransformMeta nextTransform, + IVariables variables, + IHopMetadataProvider metadataProvider) + throws HopTransformException { + // Action sink — no outgoing fields on native Spark + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableOutput.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableOutput.java new file mode 100644 index 00000000000..d94c2b30947 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableOutput.java @@ -0,0 +1,47 @@ +/* + * 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.hop.spark.transforms.table; + +import org.apache.hop.core.exception.HopException; +import org.apache.hop.pipeline.Pipeline; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.BaseTransform; +import org.apache.hop.pipeline.transform.TransformMeta; + +/** + * Metadata-only on Local engine. On the native Spark engine this becomes a lake table write action. + */ +public class SparkLakeTableOutput + extends BaseTransform { + + public SparkLakeTableOutput( + TransformMeta transformMeta, + SparkLakeTableOutputMeta meta, + SparkLakeTableOutputData data, + int copyNr, + PipelineMeta pipelineMeta, + Pipeline pipeline) { + super(transformMeta, meta, data, copyNr, pipelineMeta, pipeline); + } + + @Override + public boolean processRow() throws HopException { + setOutputDone(); + return false; + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableOutputData.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableOutputData.java new file mode 100644 index 00000000000..f756b321288 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableOutputData.java @@ -0,0 +1,27 @@ +/* + * 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.hop.spark.transforms.table; + +import org.apache.hop.pipeline.transform.BaseTransformData; +import org.apache.hop.pipeline.transform.ITransformData; + +public class SparkLakeTableOutputData extends BaseTransformData implements ITransformData { + public SparkLakeTableOutputData() { + super(); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableOutputDialog.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableOutputDialog.java new file mode 100644 index 00000000000..a2da24a4cb2 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableOutputDialog.java @@ -0,0 +1,259 @@ +/* + * 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.hop.spark.transforms.table; + +import org.apache.hop.core.Const; +import org.apache.hop.core.util.Utils; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.i18n.BaseMessages; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.spark.table.SparkLakeFormats; +import org.apache.hop.spark.transforms.io.SparkFileOutputMeta; +import org.apache.hop.ui.core.PropsUi; +import org.apache.hop.ui.core.dialog.BaseDialog; +import org.apache.hop.ui.core.widget.TextVar; +import org.apache.hop.ui.pipeline.transform.BaseTransformDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.CCombo; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +public class SparkLakeTableOutputDialog extends BaseTransformDialog { + private static final Class PKG = SparkLakeTableOutputMeta.class; + + private final SparkLakeTableOutputMeta input; + + private CCombo wFormat; + private CCombo wIdentifierMode; + private TextVar wTablePath; + private TextVar wTableIdentifier; + private TextVar wCatalogMetadataName; + private CCombo wSaveMode; + private TextVar wPartitionBy; + private TextVar wCoalesce; + private Text wExtraOptions; + + public SparkLakeTableOutputDialog( + Shell parent, + IVariables variables, + SparkLakeTableOutputMeta transformMeta, + PipelineMeta pipelineMeta) { + super(parent, variables, transformMeta, pipelineMeta); + this.input = transformMeta; + } + + @Override + public String open() { + Shell parent = getParent(); + shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.RESIZE | SWT.MIN | SWT.MAX); + PropsUi.setLook(shell); + setShellImage(shell, input); + + ModifyListener lsMod = e -> input.setChanged(); + changed = input.hasChanged(); + + FormLayout formLayout = new FormLayout(); + formLayout.marginWidth = PropsUi.getFormMargin(); + formLayout.marginHeight = PropsUi.getFormMargin(); + shell.setLayout(formLayout); + shell.setText(BaseMessages.getString(PKG, "SparkLakeTableOutputDialog.Shell.Title")); + + int middle = props.getMiddlePct(); + int margin = PropsUi.getMargin(); + + wlTransformName = new Label(shell, SWT.RIGHT); + wlTransformName.setText(BaseMessages.getString(PKG, "System.Label.TransformName")); + PropsUi.setLook(wlTransformName); + fdlTransformName = new FormData(); + fdlTransformName.left = new FormAttachment(0, 0); + fdlTransformName.top = new FormAttachment(0, margin); + fdlTransformName.right = new FormAttachment(middle, -margin); + wlTransformName.setLayoutData(fdlTransformName); + wTransformName = new Text(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + wTransformName.setText(transformName); + PropsUi.setLook(wTransformName); + wTransformName.addModifyListener(lsMod); + fdTransformName = new FormData(); + fdTransformName.left = new FormAttachment(middle, 0); + fdTransformName.top = new FormAttachment(wlTransformName, 0, SWT.CENTER); + fdTransformName.right = new FormAttachment(100, 0); + wTransformName.setLayoutData(fdTransformName); + Control last = wTransformName; + + last = labeledCombo(lsMod, middle, margin, last, "SparkLakeTableOutputDialog.Format"); + wFormat = (CCombo) last; + wFormat.setItems(new String[] {SparkLakeFormats.FORMAT_DELTA, SparkLakeFormats.FORMAT_ICEBERG}); + + last = labeledCombo(lsMod, middle, margin, last, "SparkLakeTableOutputDialog.IdentifierMode"); + wIdentifierMode = (CCombo) last; + wIdentifierMode.setItems( + new String[] {SparkLakeTableInputMeta.MODE_PATH, SparkLakeTableInputMeta.MODE_TABLE}); + + last = labeledTextVar(lsMod, middle, margin, last, "SparkLakeTableOutputDialog.TablePath"); + wTablePath = (TextVar) last; + + last = + labeledTextVar(lsMod, middle, margin, last, "SparkLakeTableOutputDialog.TableIdentifier"); + wTableIdentifier = (TextVar) last; + + last = + labeledTextVar( + lsMod, middle, margin, last, "SparkLakeTableOutputDialog.CatalogMetadataName"); + wCatalogMetadataName = (TextVar) last; + + last = labeledCombo(lsMod, middle, margin, last, "SparkLakeTableOutputDialog.SaveMode"); + wSaveMode = (CCombo) last; + wSaveMode.setItems( + new String[] { + SparkFileOutputMeta.MODE_ERROR, + SparkFileOutputMeta.MODE_APPEND, + SparkFileOutputMeta.MODE_OVERWRITE, + SparkFileOutputMeta.MODE_IGNORE + }); + + last = labeledTextVar(lsMod, middle, margin, last, "SparkLakeTableOutputDialog.PartitionBy"); + wPartitionBy = (TextVar) last; + last = labeledTextVar(lsMod, middle, margin, last, "SparkLakeTableOutputDialog.Coalesce"); + wCoalesce = (TextVar) last; + + Label wlExtra = new Label(shell, SWT.RIGHT); + wlExtra.setText(BaseMessages.getString(PKG, "SparkLakeTableOutputDialog.ExtraOptions")); + PropsUi.setLook(wlExtra); + FormData fdlExtra = new FormData(); + fdlExtra.left = new FormAttachment(0, 0); + fdlExtra.top = new FormAttachment(last, margin); + fdlExtra.right = new FormAttachment(middle, -margin); + wlExtra.setLayoutData(fdlExtra); + wExtraOptions = new Text(shell, SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.V_SCROLL); + PropsUi.setLook(wExtraOptions); + wExtraOptions.addModifyListener(lsMod); + FormData fdExtra = new FormData(); + fdExtra.left = new FormAttachment(middle, 0); + fdExtra.top = new FormAttachment(wlExtra, 0, SWT.TOP); + fdExtra.right = new FormAttachment(100, 0); + fdExtra.height = 60; + wExtraOptions.setLayoutData(fdExtra); + + wOk = new Button(shell, SWT.PUSH); + wOk.setText(BaseMessages.getString(PKG, "System.Button.OK")); + wCancel = new Button(shell, SWT.PUSH); + wCancel.setText(BaseMessages.getString(PKG, "System.Button.Cancel")); + setButtonPositions(new Button[] {wOk, wCancel}, margin, wExtraOptions); + + wOk.addListener(SWT.Selection, e -> ok()); + wCancel.addListener(SWT.Selection, e -> cancel()); + + getData(); + input.setChanged(changed); + BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel()); + return transformName; + } + + private Control labeledCombo( + ModifyListener lsMod, int middle, int margin, Control last, String labelKey) { + Label wl = new Label(shell, SWT.RIGHT); + wl.setText(BaseMessages.getString(PKG, labelKey)); + PropsUi.setLook(wl); + FormData fdl = new FormData(); + fdl.left = new FormAttachment(0, 0); + fdl.top = new FormAttachment(last, margin); + fdl.right = new FormAttachment(middle, -margin); + wl.setLayoutData(fdl); + CCombo combo = new CCombo(shell, SWT.BORDER | SWT.READ_ONLY); + PropsUi.setLook(combo); + combo.addModifyListener(lsMod); + FormData fd = new FormData(); + fd.left = new FormAttachment(middle, 0); + fd.top = new FormAttachment(wl, 0, SWT.CENTER); + fd.right = new FormAttachment(100, 0); + combo.setLayoutData(fd); + return combo; + } + + private TextVar labeledTextVar( + ModifyListener lsMod, int middle, int margin, Control last, String labelKey) { + Label wl = new Label(shell, SWT.RIGHT); + wl.setText(BaseMessages.getString(PKG, labelKey)); + PropsUi.setLook(wl); + FormData fdl = new FormData(); + fdl.left = new FormAttachment(0, 0); + fdl.top = new FormAttachment(last, margin); + fdl.right = new FormAttachment(middle, -margin); + wl.setLayoutData(fdl); + TextVar text = new TextVar(variables, shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(text); + text.addModifyListener(lsMod); + String tooltip = BaseMessages.getString(PKG, labelKey + ".Tooltip"); + if (!Utils.isEmpty(tooltip) && !tooltip.startsWith("!")) { + text.setToolTipText(tooltip); + wl.setToolTipText(tooltip); + } + FormData fd = new FormData(); + fd.left = new FormAttachment(middle, 0); + fd.top = new FormAttachment(wl, 0, SWT.CENTER); + fd.right = new FormAttachment(100, 0); + text.setLayoutData(fd); + return text; + } + + private void getData() { + wTransformName.setText(Const.NVL(transformName, "")); + wFormat.setText(Const.NVL(input.getFormat(), SparkLakeFormats.FORMAT_DELTA)); + wIdentifierMode.setText( + Const.NVL(input.getIdentifierMode(), SparkLakeTableInputMeta.MODE_PATH)); + wTablePath.setText(Const.NVL(input.getTablePath(), "")); + wTableIdentifier.setText(Const.NVL(input.getTableIdentifier(), "")); + wCatalogMetadataName.setText(Const.NVL(input.getCatalogMetadataName(), "")); + wSaveMode.setText(Const.NVL(input.getSaveMode(), SparkFileOutputMeta.MODE_ERROR)); + wPartitionBy.setText(Const.NVL(input.getPartitionByColumns(), "")); + wCoalesce.setText(Const.NVL(input.getCoalescePartitions(), "")); + wExtraOptions.setText(Const.NVL(input.getExtraOptions(), "")); + wTransformName.selectAll(); + wTransformName.setFocus(); + } + + private void cancel() { + transformName = null; + input.setChanged(changed); + dispose(); + } + + private void ok() { + if (Utils.isEmpty(wTransformName.getText())) { + return; + } + transformName = wTransformName.getText(); + input.setFormat(wFormat.getText()); + input.setIdentifierMode(wIdentifierMode.getText()); + input.setTablePath(wTablePath.getText()); + input.setTableIdentifier(wTableIdentifier.getText()); + input.setCatalogMetadataName(wCatalogMetadataName.getText()); + input.setSaveMode(wSaveMode.getText()); + input.setPartitionByColumns(wPartitionBy.getText()); + input.setCoalescePartitions(wCoalesce.getText()); + input.setExtraOptions(wExtraOptions.getText()); + dispose(); + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableOutputMeta.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableOutputMeta.java new file mode 100644 index 00000000000..70586b5c970 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/transforms/table/SparkLakeTableOutputMeta.java @@ -0,0 +1,101 @@ +/* + * 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.hop.spark.transforms.table; + +import lombok.Getter; +import lombok.Setter; +import org.apache.hop.core.annotations.Transform; +import org.apache.hop.core.exception.HopTransformException; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.metadata.api.HopMetadataProperty; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.transform.BaseTransformMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.table.SparkLakeFormats; +import org.apache.hop.spark.transforms.io.SparkFileOutputMeta; +import org.apache.hop.spark.util.SparkConst; + +@Transform( + id = SparkConst.SPARK_LAKE_TABLE_OUTPUT_PLUGIN_ID, + name = "i18n::SparkLakeTableOutput.Name", + description = "i18n::SparkLakeTableOutput.Description", + image = "spark-lake-table-output.svg", + categoryDescription = "i18n:org.apache.hop.pipeline.transform:BaseTransform.Category.BigData", + keywords = "i18n::SparkLakeTableOutput.Keyword", + documentationUrl = "/pipeline/transforms/spark-lake-table-output.html", + supportedEngines = {SparkConst.PLUGIN_ID}) +@Getter +@Setter +public class SparkLakeTableOutputMeta + extends BaseTransformMeta { + + /** {@link SparkLakeFormats#FORMAT_DELTA} or {@link SparkLakeFormats#FORMAT_ICEBERG} */ + @HopMetadataProperty(key = "format", injectionKey = "FORMAT") + private String format = SparkLakeFormats.FORMAT_DELTA; + + @HopMetadataProperty(key = "identifier_mode", injectionKey = "IDENTIFIER_MODE") + private String identifierMode = SparkLakeTableInputMeta.MODE_PATH; + + @HopMetadataProperty(key = "table_path", injectionKey = "TABLE_PATH") + private String tablePath; + + @HopMetadataProperty(key = "table_identifier", injectionKey = "TABLE_IDENTIFIER") + private String tableIdentifier; + + @HopMetadataProperty(key = "catalog_metadata_name", injectionKey = "CATALOG_METADATA_NAME") + private String catalogMetadataName; + + /** + * Default {@link SparkFileOutputMeta#MODE_ERROR} (ErrorIfExists) — ACID tables must not default + * to destructive overwrite. + */ + @HopMetadataProperty(key = "save_mode", injectionKey = "SAVE_MODE") + private String saveMode = SparkFileOutputMeta.MODE_ERROR; + + @HopMetadataProperty(key = "partition_by", injectionKey = "PARTITION_BY") + private String partitionByColumns; + + @HopMetadataProperty(key = "coalesce", injectionKey = "COALESCE") + private String coalescePartitions; + + /** Extra Spark write options as key=value lines */ + @HopMetadataProperty(key = "extra_options", injectionKey = "EXTRA_OPTIONS") + private String extraOptions; + + public SparkLakeTableOutputMeta() { + super(); + } + + @Override + public String getDialogClassName() { + return SparkLakeTableOutputDialog.class.getName(); + } + + @Override + public void getFields( + IRowMeta inputRowMeta, + String name, + IRowMeta[] info, + TransformMeta nextTransform, + IVariables variables, + IHopMetadataProvider metadataProvider) + throws HopTransformException { + // Pass-through + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/util/SparkConst.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/util/SparkConst.java new file mode 100644 index 00000000000..92d480ac183 --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/util/SparkConst.java @@ -0,0 +1,54 @@ +/* + * 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.hop.spark.util; + +public final class SparkConst { + + public static final String PLUGIN_ID = "SparkPipelineEngine"; + public static final String PLUGIN_NAME = "Native Spark pipeline engine"; + + public static final String INJECTOR_TRANSFORM_NAME = "_INJECTOR_"; + + /** + * Variable set on Spark mapPartitions mini-pipelines so nested Workflow/Pipeline Executor + * children register with the same parent id as the transform's execution-info node ({@code + * parentPipelineLogId|transformName|copyNr}). Must match the string used in engine code that + * rebinds nested parent ids (LocalWorkflowEngine / LocalPipelineEngine). + */ + public static final String VAR_TRANSFORM_OWNER_ID = "Internal.Spark.TransformOwnerId"; + + public static final String MEMORY_GROUP_BY_PLUGIN_ID = "MemoryGroupBy"; + public static final String MERGE_JOIN_PLUGIN_ID = "MergeJoin"; + public static final String UNIQUE_ROWS_PLUGIN_ID = "Unique"; + public static final String SORT_ROWS_PLUGIN_ID = "SortRows"; + public static final String GROUP_BY_PLUGIN_ID = "GroupBy"; + + public static final String SPARK_FILE_INPUT_PLUGIN_ID = "SparkFileInput"; + public static final String SPARK_FILE_OUTPUT_PLUGIN_ID = "SparkFileOutput"; + + /** Open table format (Delta / Iceberg) path and catalog I/O — native Spark only. */ + public static final String SPARK_LAKE_TABLE_INPUT_PLUGIN_ID = "SparkLakeTableInput"; + + public static final String SPARK_LAKE_TABLE_OUTPUT_PLUGIN_ID = "SparkLakeTableOutput"; + + public static final String SPARK_LAKE_TABLE_MERGE_PLUGIN_ID = "SparkLakeTableMerge"; + + public static final String SPARK_LAKE_TABLE_MAINTENANCE_PLUGIN_ID = "SparkLakeTableMaintenance"; + + private SparkConst() {} +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/util/SparkPathDialect.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/util/SparkPathDialect.java new file mode 100644 index 00000000000..cbbce8f34bc --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/util/SparkPathDialect.java @@ -0,0 +1,222 @@ +/* + * 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.hop.spark.util; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.spark.engines.ISparkPipelineEngineRunConfiguration; + +/** + * Distinguishes Hop VFS URIs (Commons VFS / {@code HopVfs}) from + * Spark/Hadoop URIs (Dataset {@code load}/{@code save}, lake PATH). + * + *

Native Spark file and lake handlers resolve variables, then optionally rewrite the URI scheme + * using the run configuration path scheme map (for example {@code s3=s3a} or {@code + * minio=s3a}), then pass the string to Spark. Classic mapPartitions transforms still use Hop VFS + * unchanged. + * + * @see org.apache.hop.core.vfs.HopVfs + */ +public final class SparkPathDialect { + + /** + * Static Hop VFS schemes that commonly confuse operators when pasted into Spark File / Lake path + * fields. Named VFS connections (MinIO, named S3, etc.) use arbitrary schemes and cannot be + * listed exhaustively. + */ + private static final Set KNOWN_HOP_VFS_SCHEMES = + Set.of("s3", "azure", "azfs", "googledrive", "dropbox", "webdav4", "webdav4s"); + + private SparkPathDialect() {} + + /** + * Extract the URI scheme (lower-case) before {@code ://}, or {@code null} if the path has no + * scheme. Also recognizes {@code file:/…} (single slash). + */ + public static String extractScheme(String path) { + if (StringUtils.isEmpty(path)) { + return null; + } + String p = path.trim(); + int sep = p.indexOf("://"); + if (sep > 0) { + return p.substring(0, sep).trim().toLowerCase(Locale.ROOT); + } + if (p.regionMatches(true, 0, "file:", 0, 5)) { + return "file"; + } + return null; + } + + /** + * Parse a path scheme map from multi-line text ({@code from=to} per line, {@code #} comments). + * Tokens may be written as {@code s3}, {@code s3://}, {@code S3A}, etc. — they are normalized to + * bare lower-case scheme names. Later lines override earlier ones for the same source scheme. + * + * @return ordered map sourceScheme → targetScheme; never {@code null} + */ + public static Map parseSchemeMap(String mapText) { + if (StringUtils.isEmpty(mapText)) { + return Collections.emptyMap(); + } + Map map = new LinkedHashMap<>(); + for (String line : mapText.split("\\r?\\n")) { + String trimmed = line.trim(); + if (trimmed.isEmpty() || trimmed.startsWith("#")) { + continue; + } + int eq = trimmed.indexOf('='); + if (eq <= 0) { + continue; + } + String from = normalizeSchemeToken(trimmed.substring(0, eq)); + String to = normalizeSchemeToken(trimmed.substring(eq + 1)); + if (StringUtils.isEmpty(from) || StringUtils.isEmpty(to)) { + continue; + } + map.put(from, to); + } + return map.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(map); + } + + /** Strip optional {@code ://} and lower-case a scheme token from the map text. */ + public static String normalizeSchemeToken(String token) { + if (StringUtils.isEmpty(token)) { + return null; + } + String t = token.trim(); + if (t.endsWith("://")) { + t = t.substring(0, t.length() - 3); + } else if (t.endsWith(":")) { + t = t.substring(0, t.length() - 1); + } + t = t.trim().toLowerCase(Locale.ROOT); + return t.isEmpty() ? null : t; + } + + /** + * Rewrite the URI scheme of {@code path} using the run configuration path scheme map, if any. + * Paths without a scheme, or whose scheme is not listed, are returned unchanged (after trim). + */ + public static String toSparkUri( + String path, ISparkPipelineEngineRunConfiguration runConfiguration) { + if (runConfiguration == null) { + return toSparkUri(path, (String) null); + } + return toSparkUri(path, runConfiguration.getPathSchemeMap()); + } + + /** + * Rewrite the URI scheme of {@code path} using multi-line map text ({@code from=to}). Empty or + * null map leaves the path unchanged (trimmed when non-empty). + */ + public static String toSparkUri(String path, String schemeMapText) { + return toSparkUri(path, parseSchemeMap(schemeMapText)); + } + + /** + * Rewrite the URI scheme of {@code path} when {@code schemeMap} contains an entry for that + * scheme. Only {@code scheme://…} forms are rewritten (not bare {@code file:/…}). + */ + public static String toSparkUri(String path, Map schemeMap) { + if (StringUtils.isEmpty(path)) { + return path; + } + String p = path.trim(); + if (schemeMap == null || schemeMap.isEmpty()) { + return p; + } + String scheme = extractScheme(p); + if (scheme == null) { + return p; + } + String target = schemeMap.get(scheme); + if (StringUtils.isEmpty(target) || target.equals(scheme)) { + return p; + } + String prefix = scheme + "://"; + if (p.regionMatches(true, 0, prefix, 0, prefix.length())) { + return target + "://" + p.substring(prefix.length()); + } + return p; + } + + /** + * {@code true} when the path uses a known static Hop VFS scheme that is not typical for Spark. + */ + public static boolean isKnownHopVfsScheme(String path) { + String scheme = extractScheme(path); + return scheme != null && KNOWN_HOP_VFS_SCHEMES.contains(scheme); + } + + /** + * Human-readable hint for a path that looks like Hop VFS, or {@code null} when no extra guidance + * applies. + */ + public static String hopVfsSchemeHint(String path) { + String scheme = extractScheme(path); + if (scheme == null || !KNOWN_HOP_VFS_SCHEMES.contains(scheme)) { + return null; + } + return switch (scheme) { + case "s3" -> + "'" + + scheme + + "://' is a Hop VFS scheme (AWS SDK). Spark File Input/Output and Lake PATH use" + + " Hadoop FileSystem URIs — prefer 's3a://…' (or map s3=s3a on the Native Spark run" + + " configuration path scheme map) with cluster S3A configuration" + + " (spark.hadoop.fs.s3a.*). Named MinIO/S3 VFS connections need a map entry" + + " (e.g. minio=s3a) plus S3A endpoint conf. See native Spark paths documentation."; + case "azure", "azfs" -> + "'" + + scheme + + "://' is a Hop VFS scheme. On Spark, Azure object storage is usually 'abfs://' or" + + " 'wasbs://' with Hadoop Azure connector configuration — not Hop VFS."; + case "googledrive", "dropbox", "webdav4", "webdav4s" -> + "'" + + scheme + + "://' is a Hop VFS scheme and is not a standard Spark/Hadoop FileSystem URI." + + " Spark File/Lake paths must be readable by every executor via Hadoop FS" + + " (file:///, hdfs://, s3a://, abfs://, …)."; + default -> + "'" + + scheme + + "://' looks like a Hop VFS scheme. Spark Dataset I/O does not use Hop VFS;" + + " use a Spark/Hadoop URI such as file:///, hdfs://, or s3a://."; + }; + } + + /** + * Append a Hop-vs-Spark path hint to an error message when the path uses a known Hop-only scheme. + * Leaves the message unchanged otherwise. + */ + public static String withPathHint(String message, String path) { + String hint = hopVfsSchemeHint(path); + if (hint == null) { + return message; + } + if (StringUtils.isEmpty(message)) { + return hint; + } + return message + " Hint: " + hint; + } +} diff --git a/plugins/engines/spark/src/main/java/org/apache/hop/spark/util/SparkRunMode.java b/plugins/engines/spark/src/main/java/org/apache/hop/spark/util/SparkRunMode.java new file mode 100644 index 00000000000..286086dddef --- /dev/null +++ b/plugins/engines/spark/src/main/java/org/apache/hop/spark/util/SparkRunMode.java @@ -0,0 +1,182 @@ +/* + * 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.hop.spark.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.engines.ISparkPipelineEngineRunConfiguration; + +/** + * Effective execution mode for generic (mapPartitions) Hop transforms on the native Spark engine. + * + *

{@link #DISTRIBUTED} runs {@code mapPartitions} across the input Dataset partitions (default). + * {@link #DRIVER_ONLY} materializes the input on the Spark driver and runs the Hop mini-pipeline + * once there, so nested work (Workflow Executor, etc.) does not fan out across executors. + * + *

Per-transform override is stored on {@link TransformMeta#attributesMap} under group {@link + * #ATTRIBUTE_GROUP} / key {@link #ATTRIBUTE_KEY}. + */ +public enum SparkRunMode { + DISTRIBUTED, + DRIVER_ONLY; + + /** Transform attribute group for Spark-specific canvas settings. */ + public static final String ATTRIBUTE_GROUP = "spark"; + + /** Transform attribute key for the run-mode override. */ + public static final String ATTRIBUTE_KEY = "run_mode"; + + /** Inherit the pipeline run-configuration default (or absent attribute). */ + public static final String OVERRIDE_INHERIT = "INHERIT"; + + /** Force {@link #DISTRIBUTED} regardless of run configuration. */ + public static final String OVERRIDE_FORCE_DISTRIBUTED = "FORCE_DISTRIBUTED"; + + /** Force {@link #DRIVER_ONLY} regardless of run configuration. */ + public static final String OVERRIDE_FORCE_DRIVER_ONLY = "FORCE_DRIVER_ONLY"; + + public static final String DISPLAY_INHERIT = "Inherit"; + public static final String DISPLAY_FORCE_DISTRIBUTED = "Force distributed"; + public static final String DISPLAY_FORCE_DRIVER_ONLY = "Force Driver Only"; + + /** Parse a run-configuration value. Unknown / empty values default to {@link #DISTRIBUTED}. */ + public static SparkRunMode parseConfigValue(String raw) { + if (StringUtils.isBlank(raw)) { + return DISTRIBUTED; + } + String n = raw.trim(); + if (DRIVER_ONLY.name().equalsIgnoreCase(n) + || "DRIVER ONLY".equalsIgnoreCase(n) + || "DRIVER-ONLY".equalsIgnoreCase(n)) { + return DRIVER_ONLY; + } + return DISTRIBUTED; + } + + /** + * Parse a transform override attribute. Unknown / empty values mean {@link #OVERRIDE_INHERIT}. + */ + public static String parseOverride(String raw) { + if (StringUtils.isBlank(raw)) { + return OVERRIDE_INHERIT; + } + String n = raw.trim(); + if (OVERRIDE_FORCE_DISTRIBUTED.equalsIgnoreCase(n) || "FORCE DISTRIBUTED".equalsIgnoreCase(n)) { + return OVERRIDE_FORCE_DISTRIBUTED; + } + if (OVERRIDE_FORCE_DRIVER_ONLY.equalsIgnoreCase(n) + || "FORCE DRIVER ONLY".equalsIgnoreCase(n) + || "FORCE_DRIVER".equalsIgnoreCase(n)) { + return OVERRIDE_FORCE_DRIVER_ONLY; + } + if (OVERRIDE_INHERIT.equalsIgnoreCase(n)) { + return OVERRIDE_INHERIT; + } + // Legacy / accidental storage of effective mode names as override + if (DRIVER_ONLY.name().equalsIgnoreCase(n)) { + return OVERRIDE_FORCE_DRIVER_ONLY; + } + if (DISTRIBUTED.name().equalsIgnoreCase(n)) { + return OVERRIDE_FORCE_DISTRIBUTED; + } + return OVERRIDE_INHERIT; + } + + /** Read the raw override from a transform (never null; defaults to inherit). */ + public static String getOverride(TransformMeta transformMeta) { + if (transformMeta == null) { + return OVERRIDE_INHERIT; + } + return parseOverride(transformMeta.getAttribute(ATTRIBUTE_GROUP, ATTRIBUTE_KEY)); + } + + /** Store or clear the override on a transform. Inherit removes the attribute when possible. */ + public static void setOverride(TransformMeta transformMeta, String overrideCode) { + if (transformMeta == null) { + return; + } + String code = parseOverride(overrideCode); + if (OVERRIDE_INHERIT.equals(code)) { + Map attrs = transformMeta.getAttributes(ATTRIBUTE_GROUP); + if (attrs != null) { + attrs.remove(ATTRIBUTE_KEY); + if (attrs.isEmpty()) { + transformMeta.getAttributesMap().remove(ATTRIBUTE_GROUP); + } + } + return; + } + transformMeta.setAttribute(ATTRIBUTE_GROUP, ATTRIBUTE_KEY, code); + } + + /** Resolve the effective mode for a generic transform from override + run configuration. */ + public static SparkRunMode resolve( + TransformMeta transformMeta, ISparkPipelineEngineRunConfiguration runConfiguration) { + String override = getOverride(transformMeta); + if (OVERRIDE_FORCE_DISTRIBUTED.equals(override)) { + return DISTRIBUTED; + } + if (OVERRIDE_FORCE_DRIVER_ONLY.equals(override)) { + return DRIVER_ONLY; + } + String configValue = + runConfiguration != null ? runConfiguration.getGenericTransformRunMode() : null; + return parseConfigValue(configValue); + } + + /** Combo values for the Native Spark run configuration. */ + public static List configDisplayValues() { + List list = new ArrayList<>(2); + list.add(DISTRIBUTED.name()); + list.add(DRIVER_ONLY.name()); + return list; + } + + /** Labels for the transform context-action selection dialog. */ + public static String[] overrideDisplayLabels() { + return new String[] {DISPLAY_INHERIT, DISPLAY_FORCE_DISTRIBUTED, DISPLAY_FORCE_DRIVER_ONLY}; + } + + public static String overrideFromDisplayLabel(String label) { + if (DISPLAY_FORCE_DISTRIBUTED.equals(label)) { + return OVERRIDE_FORCE_DISTRIBUTED; + } + if (DISPLAY_FORCE_DRIVER_ONLY.equals(label)) { + return OVERRIDE_FORCE_DRIVER_ONLY; + } + return OVERRIDE_INHERIT; + } + + public static String displayLabelForOverride(String overrideCode) { + String code = parseOverride(overrideCode); + return switch (code) { + case OVERRIDE_FORCE_DISTRIBUTED -> DISPLAY_FORCE_DISTRIBUTED; + case OVERRIDE_FORCE_DRIVER_ONLY -> DISPLAY_FORCE_DRIVER_ONLY; + default -> DISPLAY_INHERIT; + }; + } + + /** True when a non-inherit override should paint a canvas badge. */ + public static boolean hasForcedOverride(TransformMeta transformMeta) { + String o = getOverride(transformMeta); + return OVERRIDE_FORCE_DISTRIBUTED.equals(o) || OVERRIDE_FORCE_DRIVER_ONLY.equals(o); + } +} diff --git a/plugins/engines/spark/src/main/resources/dependencies.xml b/plugins/engines/spark/src/main/resources/dependencies.xml new file mode 100644 index 00000000000..55ac030df2e --- /dev/null +++ b/plugins/engines/spark/src/main/resources/dependencies.xml @@ -0,0 +1,38 @@ + + + + ../../transforms/memgroupby + ../../transforms/mergejoin + ../../transforms/uniquerows + ../../transforms/sort + + ../../transforms/constant + ../../transforms/filterrows + ../../transforms/switchcase + ../../transforms/calculator + ../../transforms/selectvalues + ../../transforms/dummy + ../../transforms/rowgenerator + ../../transforms/streamlookup + ../../transforms/stringoperations + + ../../transforms/textfile + ../../transforms/getfilenames + ../../transforms/excel + diff --git a/plugins/engines/spark/src/main/resources/org/apache/hop/spark/engines/messages/messages_en_US.properties b/plugins/engines/spark/src/main/resources/org/apache/hop/spark/engines/messages/messages_en_US.properties new file mode 100644 index 00000000000..4483c854f33 --- /dev/null +++ b/plugins/engines/spark/src/main/resources/org/apache/hop/spark/engines/messages/messages_en_US.properties @@ -0,0 +1,46 @@ +# +# 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. +# + +SparkEngine.LoadTemplate.Label=Load configuration template +SparkEngine.LoadTemplate.ToolTip=Fill master, memory, and Spark config fields from a common deployment template (local, cluster, Databricks, …) +SparkEngine.LoadTemplate.Dialog.Title=Load Spark configuration template +SparkEngine.LoadTemplate.Dialog.Message=Choose a template. Values replace the current engine options (you can edit them afterward). +SparkEngine.LoadTemplate.Confirm.Title=Replace configuration? +SparkEngine.LoadTemplate.Confirm.Message=Replace the current Native Spark options with template "{0}"? + +SparkEngine.OptionsMaster.Label=Spark master +SparkEngine.OptionsMaster.ToolTip=Spark master URL, e.g. local[*], local[4], or spark://host:7077. Leave empty under spark-submit so the CLI master is used. +SparkEngine.OptionsAppName.Label=Application name +SparkEngine.OptionsAppName.ToolTip=Spark application name +SparkEngine.OptionsFatJar.Label=Fat jar file location +SparkEngine.OptionsFatJar.ToolTip=Optional path to a Hop fat jar staged on executors for cluster runs. Leave empty for pure spark-submit with a native-provided fat jar. +SparkEngine.OptionsConfigs.Label=Spark config (key=value lines) +SparkEngine.OptionsConfigs.ToolTip=Additional SparkConf entries, one key=value per line +SparkEngine.OptionsDriverMemory.Label=Driver memory +SparkEngine.OptionsDriverMemory.ToolTip=spark.driver.memory, e.g. 2g +SparkEngine.OptionsExecutorMemory.Label=Executor memory +SparkEngine.OptionsExecutorMemory.ToolTip=spark.executor.memory, e.g. 2g +SparkEngine.OptionsExecutorCores.Label=Executor cores +SparkEngine.OptionsExecutorCores.ToolTip=spark.executor.cores +SparkEngine.OptionsTempLocation.Label=Temp location +SparkEngine.OptionsTempLocation.ToolTip=Directory for temporary files +SparkEngine.OptionsPluginsToStage.Label=Plugins to stage +SparkEngine.OptionsPluginsToStage.ToolTip=Optional comma-separated plugin folders to stage on the cluster +SparkEngine.OptionsPathSchemeMap.Label=Path scheme map (from=to lines) +SparkEngine.OptionsPathSchemeMap.ToolTip=Optional URI scheme rewrite for Spark File / Lake PATH only (not classic Hop VFS). One from=to per line, e.g. s3=s3a or minio=s3a. Comments start with #. Still configure spark.hadoop.fs.s3a.* (or other FS) for the target scheme. +SparkEngine.OptionsGenericRunMode.Label=Generic transform run mode +SparkEngine.OptionsGenericRunMode.ToolTip=Default for Hop transforms that run as mapPartitions (Workflow Executor, most classic transforms). DISTRIBUTED fans out across Spark partitions. DRIVER_ONLY runs the mini-pipeline once on the Spark driver so nested workflows do not execute on multiple nodes. Override per transform with the "Spark Run Mode" context action. diff --git a/plugins/engines/spark/src/main/resources/org/apache/hop/spark/gui/messages/messages_en_US.properties b/plugins/engines/spark/src/main/resources/org/apache/hop/spark/gui/messages/messages_en_US.properties new file mode 100644 index 00000000000..d024b893417 --- /dev/null +++ b/plugins/engines/spark/src/main/resources/org/apache/hop/spark/gui/messages/messages_en_US.properties @@ -0,0 +1,58 @@ +# +# 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. +# + +SparkGuiPlugin.Menu.SparkPackageConfig.Text=Edit Native Spark package filters... +SparkGuiPlugin.Menu.ExportSparkProjectPackage.Text=Export project package for Native Spark... +SparkGuiPlugin.ExportSparkProject.Dialog.Header=Export for Native Spark execution +SparkGuiPlugin.ExportSparkProject.Dialog.Message1=This builds a project package zip specifically for running pipelines on the Native Spark engine (MainSpark --HopProjectPackage). +SparkGuiPlugin.ExportSparkProject.Dialog.Message2=It includes project definition files (pipelines, workflows, small resources under project/) plus metadata.json. Defaults skip work/, datasets/, target/, and other bulk/IDE dirs (configurable via Tools → Edit Native Spark package filters / spark-package.json). This is not the same as File → Export current project to zip or .gitignore. +SparkGuiPlugin.ExportSparkProject.Dialog.Message3=On Databricks, packages under /Volumes are shared without SparkFiles; elsewhere the zip is distributed so Simple Mapping and Pipeline Executor paths under ${PROJECT_HOME} resolve. Use separate variables (HOP_DATA / s3a://…) for Spark File data paths — not PROJECT_HOME. + +SparkPackageConfigDialog.Shell.Title=Native Spark package filters +SparkPackageConfigDialog.Intro=Configure which project files are included when exporting a Native Spark project package (Tools → Export project package, Databricks Deploy & run, hop-conf). Saved as spark-package.json in the project home. Not the same as .gitignore. +SparkPackageConfigDialog.ProjectHome.Label=Project home +SparkPackageConfigDialog.Defaults.Label=Built-in exclude directories (always applied unless “Replace defaults” is checked): {0} +SparkPackageConfigDialog.ReplaceDefaults.Label=Replace built-in exclude directories (use only lists below) +SparkPackageConfigDialog.ExcludeDirs.Label=Extra exclude directories +SparkPackageConfigDialog.ExcludeDirs.Tooltip=One directory basename per line (e.g. tmp, screenshots). Merged with built-in excludes unless Replace defaults is checked. +SparkPackageConfigDialog.ExcludeGlobs.Label=Exclude globs +SparkPackageConfigDialog.ExcludeGlobs.Tooltip=One glob per line, relative to project home (e.g. **/*.jar or mappings/large/**). +SparkPackageConfigDialog.IncludePaths.Label=Force-include paths +SparkPackageConfigDialog.IncludePaths.Tooltip=One relative path per line to include even under a skipped directory (e.g. work/cluster-env.json). +SparkPackageConfigDialog.Error.Load.Title=Load failed +SparkPackageConfigDialog.Error.Load.Message=Could not read spark-package.json +SparkPackageConfigDialog.Error.Save.Title=Save failed +SparkPackageConfigDialog.Error.Save.Message=Could not write spark-package.json +SparkGuiPlugin.ExportSparkProject.NoProject.Header=No active project +SparkGuiPlugin.ExportSparkProject.NoProject.Message=PROJECT_HOME is not set. Open or enable a Hop project first, then export again. +SparkGuiPlugin.ExportSparkProject.Progress.Message=Exporting Native Spark project package... +SparkGuiPlugin.ExportSparkProject.Done.Header=Native Spark package created +SparkGuiPlugin.ExportSparkProject.Done.Message1=Package written to: {0} +SparkGuiPlugin.ExportSparkProject.Done.Message2=The path was copied to the clipboard. +SparkGuiPlugin.ExportSparkProject.Done.Message3=Submit with MainSpark --HopProjectPackage= --HopPipelinePath= --HopRunConfigurationName=. The engine distributes the zip to all executors automatically. +SparkGuiPlugin.ExportSparkProject.Error.Header=Export failed +SparkGuiPlugin.ExportSparkProject.Error.Message=Could not export the Native Spark project package +SparkGuiPlugin.FileTypes.Zip.Label=Zip files (*.zip) +SparkGuiPlugin.FileTypes.All.Label=All Files (*.*) + +SparkGuiPlugin.ContextAction.Category=Spark +SparkGuiPlugin.ContextAction.SparkRunMode.Name=Spark Run Mode +SparkGuiPlugin.ContextAction.SparkRunMode.Tooltip=Override Native Spark generic transform execution: Inherit the run configuration default, force DISTRIBUTED (mapPartitions on executors), or force DRIVER_ONLY (run once on the Spark driver). Useful for Workflow Executor and other nested executions. +SparkGuiPlugin.ContextAction.SparkRunMode.Dialog.Title=Spark Run Mode +SparkGuiPlugin.ContextAction.SparkRunMode.Dialog.Message=Choose how transform "{0}" should run on the Native Spark engine when it is handled as a generic mapPartitions transform. Current: {1} +SparkGuiPlugin.ContextAction.SparkRunMode.Error.Header=Spark Run Mode +SparkGuiPlugin.ContextAction.SparkRunMode.Error.Message=Could not set the Spark run mode for this transform diff --git a/plugins/engines/spark/src/main/resources/org/apache/hop/spark/metadata/messages/messages_en_US.properties b/plugins/engines/spark/src/main/resources/org/apache/hop/spark/metadata/messages/messages_en_US.properties new file mode 100644 index 00000000000..374326f0c7b --- /dev/null +++ b/plugins/engines/spark/src/main/resources/org/apache/hop/spark/metadata/messages/messages_en_US.properties @@ -0,0 +1,40 @@ +# +# 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. +# + +SparkCatalog.Name=Spark Catalog +SparkCatalog.Description=Spark SQL catalog for open table formats (Iceberg Hadoop/REST, custom) +SparkCatalog.Name.Label=Name +SparkCatalog.CatalogName.Label=Spark catalog name +SparkCatalog.CatalogName.Tooltip=Name used in spark.sql.catalog. (e.g. lake). Must match the first part of table identifiers. +SparkCatalog.CatalogType.Label=Catalog type +SparkCatalog.CatalogType.Tooltip=hadoop (warehouse), rest (uri), custom (implementation), or advanced hive/glue via extra conf +SparkCatalog.Implementation.Label=Implementation class +SparkCatalog.Implementation.Tooltip=Full class name (default org.apache.iceberg.spark.SparkCatalog). Required for custom type. +SparkCatalog.Warehouse.Label=Warehouse +SparkCatalog.Warehouse.Tooltip=Warehouse path/URI (hadoop type; file:// or s3a://...) +SparkCatalog.Uri.Label=URI +SparkCatalog.Uri.Tooltip=REST catalog endpoint (or thrift URI for advanced hive) +SparkCatalog.Credential.Label=Credential / token +SparkCatalog.Credential.Tooltip=Optional secret (password field). Not logged. For REST catalogs this maps to spark.sql.catalog..token when set. +SparkCatalog.ConfExtra.Label=Extra conf (key=value lines) +SparkCatalog.ConfExtra.Tooltip=One key=value per line (# comments allowed). Short keys become spark.sql.catalog..key. Treat as sensitive if tokens are included. +SparkCatalog.LoadTemplate.Label=Load catalog template +SparkCatalog.LoadTemplate.ToolTip=Fill the form from a named catalog scenario (Iceberg Hadoop/REST, Hive, Glue, \u2026). Replace placeholders for your environment. +SparkCatalog.LoadTemplate.Dialog.Title=Load catalog template +SparkCatalog.LoadTemplate.Dialog.Message=Choose a catalog scenario. Placeholders (host, bucket, URI) must be adjusted for your environment. +SparkCatalog.LoadTemplate.Confirm.Title=Replace catalog settings? +SparkCatalog.LoadTemplate.Confirm.Message=Replace the current catalog fields with template "{0}"? diff --git a/plugins/engines/spark/src/main/resources/org/apache/hop/spark/transforms/io/messages/messages_en_US.properties b/plugins/engines/spark/src/main/resources/org/apache/hop/spark/transforms/io/messages/messages_en_US.properties new file mode 100644 index 00000000000..34f9ee6e654 --- /dev/null +++ b/plugins/engines/spark/src/main/resources/org/apache/hop/spark/transforms/io/messages/messages_en_US.properties @@ -0,0 +1,51 @@ +# +# 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. +# + +SparkFileInput.Name=Spark File Input +SparkFileInput.Description=Read files with the native Spark engine (CSV, Parquet, JSON, ORC). Path is a Spark/Hadoop URI, not Hop VFS. +SparkFileInput.Keyword=spark,file,input,csv,parquet,json + +SparkFileOutput.Name=Spark File Output +SparkFileOutput.Description=Write files with the native Spark engine (CSV, Parquet, JSON, ORC). Path is a Spark/Hadoop URI, not Hop VFS. +SparkFileOutput.Keyword=spark,file,output,csv,parquet,json + +SparkFileInputDialog.Shell.Title=Spark File Input +SparkFileInputDialog.FilePath=Spark path (Hadoop URI) +SparkFileInputDialog.FilePath.Tooltip=URI visible to Spark on every executor (not Hop VFS). Examples\: file path on local[*], hdfs\://…, s3a\://bucket/prefix. Do not use Hop-only schemes such as s3\:// or named MinIO (minio\://). Avoid ${PROJECT_HOME} here when using a Spark project package — that variable points at definition files, not data; use HOP_DATA / OUTPUT_ROOT or s3a\:// for Dataset paths. +SparkFileInputDialog.Format=Format +SparkFileInputDialog.Header=First line is header +SparkFileInputDialog.Separator=Separator +SparkFileInputDialog.Quote=Quote character +SparkFileInputDialog.InferSchema=Infer schema (CSV without field list) +SparkFileInputDialog.ExtraOptions=Extra options (key=value lines) +SparkFileInputDialog.Fields=Fields (optional schema) +SparkFileInputDialog.Column.Name=Name +SparkFileInputDialog.Column.Type=Type +SparkFileInputDialog.Column.Length=Length +SparkFileInputDialog.Column.Precision=Precision + +SparkFileOutputDialog.Shell.Title=Spark File Output +SparkFileOutputDialog.FilePath=Spark path / directory (Hadoop URI) +SparkFileOutputDialog.FilePath.Tooltip=Output directory or prefix Spark should write to (Hadoop FileSystem URI). Examples\: local path, hdfs\://…, s3a\://bucket/prefix. Not a Hop VFS path. Avoid ${PROJECT_HOME} with a Spark project package (definitions only); use HOP_DATA / OUTPUT_ROOT or s3a\:// for data. +SparkFileOutputDialog.Format=Format +SparkFileOutputDialog.SaveMode=Save mode +SparkFileOutputDialog.Header=Write header +SparkFileOutputDialog.Separator=Separator +SparkFileOutputDialog.Quote=Quote character +SparkFileOutputDialog.PartitionBy=Partition by columns (comma-separated) +SparkFileOutputDialog.Coalesce=Coalesce partitions (e.g. 1 for single file) +SparkFileOutputDialog.ExtraOptions=Extra options (key=value lines) diff --git a/plugins/engines/spark/src/main/resources/org/apache/hop/spark/transforms/table/messages/messages_en_US.properties b/plugins/engines/spark/src/main/resources/org/apache/hop/spark/transforms/table/messages/messages_en_US.properties new file mode 100644 index 00000000000..9cc9d627ea5 --- /dev/null +++ b/plugins/engines/spark/src/main/resources/org/apache/hop/spark/transforms/table/messages/messages_en_US.properties @@ -0,0 +1,89 @@ +# +# 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. +# + +SparkLakeTableInput.Name=Spark Lake Table Input +SparkLakeTableInput.Description=Read an open table format (Delta Lake / Apache Iceberg) with the native Spark engine +SparkLakeTableInput.Keyword=spark,delta,iceberg,lake,table,input,acid +SparkLakeTableInput.Injection.Group.Fields=Fields (optional projection) + +SparkLakeTableOutput.Name=Spark Lake Table Output +SparkLakeTableOutput.Description=Write an open table format (Delta Lake / Apache Iceberg) with the native Spark engine +SparkLakeTableOutput.Keyword=spark,delta,iceberg,lake,table,output,acid + +SparkLakeTableInputDialog.Shell.Title=Spark Lake Table Input +SparkLakeTableInputDialog.Format=Table format +SparkLakeTableInputDialog.IdentifierMode=Identifier mode +SparkLakeTableInputDialog.TablePath=Spark table path (PATH mode, Hadoop URI) +SparkLakeTableInputDialog.TablePath.Tooltip=Lake table root as a Spark/Hadoop URI (file\:///…, s3a\://…, …). Not a Hop VFS path (s3\://, named MinIO connection schemes). +SparkLakeTableInputDialog.TableIdentifier=Table identifier (TABLE mode, e.g. lake.db.orders) +SparkLakeTableInputDialog.CatalogMetadataName=Spark Catalog metadata name (TABLE mode) +SparkLakeTableInputDialog.TimeTravelType=Time travel +SparkLakeTableInputDialog.TimeTravelVersion=Version / snapshot id +SparkLakeTableInputDialog.TimeTravelTimestamp=As-of timestamp +SparkLakeTableInputDialog.ExtraOptions=Extra options (key=value lines) +SparkLakeTableInputDialog.Fields=Fields (optional projection) +SparkLakeTableInputDialog.Column.Name=Name +SparkLakeTableInputDialog.Column.Type=Type +SparkLakeTableInputDialog.Column.Length=Length +SparkLakeTableInputDialog.Column.Precision=Precision + +SparkLakeTableOutputDialog.Shell.Title=Spark Lake Table Output +SparkLakeTableOutputDialog.Format=Table format +SparkLakeTableOutputDialog.IdentifierMode=Identifier mode +SparkLakeTableOutputDialog.TablePath=Spark table path (PATH mode, Hadoop URI) +SparkLakeTableOutputDialog.TablePath.Tooltip=Lake table root as a Spark/Hadoop URI (file\:///…, s3a\://…, …). Not a Hop VFS path. +SparkLakeTableOutputDialog.TableIdentifier=Table identifier (TABLE mode, e.g. lake.db.orders) +SparkLakeTableOutputDialog.CatalogMetadataName=Spark Catalog metadata name (TABLE mode) +SparkLakeTableOutputDialog.SaveMode=Save mode +SparkLakeTableOutputDialog.PartitionBy=Partition by columns (comma-separated) +SparkLakeTableOutputDialog.Coalesce=Coalesce partitions (e.g. 1 for single file) +SparkLakeTableOutputDialog.ExtraOptions=Extra options (key=value lines) + +SparkLakeTableMerge.Name=Spark Lake Table Merge +SparkLakeTableMerge.Description=MERGE INTO (upsert) a Delta Lake or Apache Iceberg table with the native Spark engine +SparkLakeTableMerge.Keyword=spark,delta,iceberg,lake,table,merge,upsert,acid + +SparkLakeTableMergeDialog.Shell.Title=Spark Lake Table Merge +SparkLakeTableMergeDialog.Format=Table format +SparkLakeTableMergeDialog.IdentifierMode=Identifier mode +SparkLakeTableMergeDialog.TablePath=Spark target table path (PATH mode, Hadoop URI) +SparkLakeTableMergeDialog.TablePath.Tooltip=Target lake table root as a Spark/Hadoop URI (file\:///…, s3a\://…, …). Not a Hop VFS path. +SparkLakeTableMergeDialog.TableIdentifier=Target table identifier (TABLE mode) +SparkLakeTableMergeDialog.CatalogMetadataName=Spark Catalog metadata name (TABLE mode) +SparkLakeTableMergeDialog.MergeCondition=Merge condition (ON clause, t=target s=source) +SparkLakeTableMergeDialog.MatchedAction=When matched +SparkLakeTableMergeDialog.NotMatchedAction=When not matched +SparkLakeTableMergeDialog.NotMatchedBySourceAction=When not matched by source (advanced) +SparkLakeTableMergeDialog.RawMergeSql=Raw MERGE SQL (advanced; overrides structured fields) + +SparkLakeTableMaintenance.Name=Spark Lake Table Maintenance +SparkLakeTableMaintenance.Description=OPTIMIZE, VACUUM, expire snapshots, or DELETE on a Delta/Iceberg table (native Spark) +SparkLakeTableMaintenance.Keyword=spark,delta,iceberg,optimize,vacuum,maintenance,compact + +SparkLakeTableMaintenanceDialog.Shell.Title=Spark Lake Table Maintenance +SparkLakeTableMaintenanceDialog.Format=Table format +SparkLakeTableMaintenanceDialog.IdentifierMode=Identifier mode +SparkLakeTableMaintenanceDialog.TablePath=Spark table path (PATH mode, Hadoop URI) +SparkLakeTableMaintenanceDialog.TablePath.Tooltip=Lake table root as a Spark/Hadoop URI (file\:///…, s3a\://…, …). Not a Hop VFS path. +SparkLakeTableMaintenanceDialog.TableIdentifier=Table identifier (TABLE mode) +SparkLakeTableMaintenanceDialog.CatalogMetadataName=Spark Catalog metadata name (TABLE mode) +SparkLakeTableMaintenanceDialog.Operation=Operation +SparkLakeTableMaintenanceDialog.RetentionHours=Retention hours (VACUUM / EXPIRE required) +SparkLakeTableMaintenanceDialog.RetainLast=Retain last N snapshots (Iceberg expire) +SparkLakeTableMaintenanceDialog.WhereClause=WHERE clause (OPTIMIZE / DELETE) +SparkLakeTableMaintenanceDialog.ZOrderColumns=ZORDER columns (Delta OPTIMIZE, comma-separated) +SparkLakeTableMaintenanceDialog.AcknowledgeDestructive=I understand this may delete data files / snapshots diff --git a/plugins/engines/spark/src/main/resources/spark-catalog.svg b/plugins/engines/spark/src/main/resources/spark-catalog.svg new file mode 100644 index 00000000000..dd89035553f --- /dev/null +++ b/plugins/engines/spark/src/main/resources/spark-catalog.svg @@ -0,0 +1,5 @@ + + + + diff --git a/plugins/engines/spark/src/main/resources/spark-file-input.svg b/plugins/engines/spark/src/main/resources/spark-file-input.svg new file mode 100644 index 00000000000..dd89035553f --- /dev/null +++ b/plugins/engines/spark/src/main/resources/spark-file-input.svg @@ -0,0 +1,5 @@ + + + + diff --git a/plugins/engines/spark/src/main/resources/spark-file-output.svg b/plugins/engines/spark/src/main/resources/spark-file-output.svg new file mode 100644 index 00000000000..e3fb4293a69 --- /dev/null +++ b/plugins/engines/spark/src/main/resources/spark-file-output.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/engines/spark/src/main/resources/spark-lake-table-input.svg b/plugins/engines/spark/src/main/resources/spark-lake-table-input.svg new file mode 100644 index 00000000000..dd89035553f --- /dev/null +++ b/plugins/engines/spark/src/main/resources/spark-lake-table-input.svg @@ -0,0 +1,5 @@ + + + + diff --git a/plugins/engines/spark/src/main/resources/spark-lake-table-maintenance.svg b/plugins/engines/spark/src/main/resources/spark-lake-table-maintenance.svg new file mode 100644 index 00000000000..e3fb4293a69 --- /dev/null +++ b/plugins/engines/spark/src/main/resources/spark-lake-table-maintenance.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/engines/spark/src/main/resources/spark-lake-table-merge.svg b/plugins/engines/spark/src/main/resources/spark-lake-table-merge.svg new file mode 100644 index 00000000000..e3fb4293a69 --- /dev/null +++ b/plugins/engines/spark/src/main/resources/spark-lake-table-merge.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/engines/spark/src/main/resources/spark-lake-table-output.svg b/plugins/engines/spark/src/main/resources/spark-lake-table-output.svg new file mode 100644 index 00000000000..e3fb4293a69 --- /dev/null +++ b/plugins/engines/spark/src/main/resources/spark-lake-table-output.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/engines/spark/src/main/resources/spark-run-distributed.svg b/plugins/engines/spark/src/main/resources/spark-run-distributed.svg new file mode 100644 index 00000000000..c555587c68d --- /dev/null +++ b/plugins/engines/spark/src/main/resources/spark-run-distributed.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/plugins/engines/spark/src/main/resources/spark-run-driver.svg b/plugins/engines/spark/src/main/resources/spark-run-driver.svg new file mode 100644 index 00000000000..b1618fb3004 --- /dev/null +++ b/plugins/engines/spark/src/main/resources/spark-run-driver.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/plugins/engines/spark/src/main/resources/version.xml b/plugins/engines/spark/src/main/resources/version.xml new file mode 100644 index 00000000000..07721296d60 --- /dev/null +++ b/plugins/engines/spark/src/main/resources/version.xml @@ -0,0 +1,19 @@ + + +${project.version} diff --git a/plugins/engines/spark/src/samples/spark-demo/README.md b/plugins/engines/spark/src/samples/spark-demo/README.md new file mode 100644 index 00000000000..b346a76851d --- /dev/null +++ b/plugins/engines/spark/src/samples/spark-demo/README.md @@ -0,0 +1,167 @@ + + +# spark-demo + +Sample Hop project for the **Native Spark** cluster walk-through: prove that nested +artifacts under `${PROJECT_HOME}` (mappings, workflows) run on a multi-node Spark +cluster when you submit a **project package** with `MainSpark`. + +## What is in the box + +| Path | Role | +|------|------| +| `pipelines/01-enrich-with-mapping.hpl` | Parent: Spark File Input → Simple Mapping → Spark File Output | +| `pipelines/mappings/upper-name.hpl` | Child mapping (must load via package `PROJECT_HOME`) | +| `pipelines/02-run-workflows.hpl` | Spark File Input → Workflow Executor (one workflow per country) | +| `workflows/create-country-folder.hwf` | Child workflow: create `${HOP_DATA}/out/countries/${COUNTRY_NAME}` + marker file | +| `pipelines/03-run-pipelines.hpl` | Orchestrator: Pipeline Executor (Force Driver Only) → nested **Native Spark** child per country | +| `pipelines/nested/enrich-by-country.hpl` | Child Spark Dataset job: customers → `out/by-country/${COUNTRY_NAME}/` | +| `data/customers-sample.csv` | Mapping / nested-pipeline demo input | +| `data/countries.csv` | Workflow + nested-pipeline control keys (Genovia, Wakanda, Latveria) | +| `scripts/spark-submit-pipelines-demo.sh` | Submit `03-run-pipelines.hpl` | +| `metadata/.../spark-cluster.json` | Native Spark run configuration for `spark-submit` | +| `cluster-env.json` | Sets `HOP_DATA=file:///data/hop-data` for the cluster | +| `scripts/prepare-dist.sh` | Host: fat jar + package zip into `/tmp/spark-demo-dist` (override with `DIST_DIR`) | +| `scripts/spark-submit-demo.sh` | Inside master: `spark-submit` + MainSpark package args | +| `scripts/spark-submit-workflows-demo.sh` | Same, for `pipelines/02-run-workflows.hpl` | + +**Data vs definitions:** Dataset and side-effect paths use **`HOP_DATA`**. Mapping and +workflow definition paths use **`${PROJECT_HOME}/…`**. Do not put bulk data under +package `PROJECT_HOME`. + +## Five-command happy path (mapping) + +From the **repository root**, with a Hop install that includes the native Spark plugin +(`HOP_HOME` or `./hop-conf.sh`): + +```bash +# 1) Fat jar + project package → /tmp/spark-demo-dist (no hop-config registration needed) +./plugins/engines/spark/src/samples/spark-demo/scripts/prepare-dist.sh + +# 2) Start Spark 4.1 master + workers +# /tmp/spark-demo-dist → /opt/hop-dist (artifacts) +# /tmp/spark-demo-dist/hop-data → /data/hop-data (shared data plane, host-visible) +docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml \ + up -d --build --scale spark-worker=2 + +# 3) Submit mapping demo +docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml \ + exec spark /opt/hop-samples/spark-demo/scripts/spark-submit-demo.sh + +# 4) Inspect on the host (same files as /data/hop-data in the containers) +ls -la /tmp/spark-demo-dist/hop-data/out/enriched +# Optional: execution information location JSON +ls -la /tmp/spark-demo-dist/hop-data/executions 2>/dev/null || true + +# 5) Tear down +docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml down +``` + +## Workflow Executor demo (countries) + +Same cluster; submit the second pipeline: + +```bash +docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml \ + exec spark /opt/hop-samples/spark-demo/scripts/spark-submit-workflows-demo.sh +``` + +Or: + +```bash +docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml \ + exec spark env PIPELINE_PATH=pipelines/02-run-workflows.hpl \ + /opt/hop-samples/spark-demo/scripts/spark-submit-demo.sh +``` + +### What to inspect + +On the **host** (bind mount): + +```bash +find /tmp/spark-demo-dist/hop-data/out/countries -type f | sort +ls -la /tmp/spark-demo-dist/hop-data/executions 2>/dev/null || true +``` + +Or inside the container (`/data/hop-data` is the same directory): + +```bash +docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml \ + exec spark find /data/hop-data/out/countries -type f | sort +``` + +Expected layout: + +```text +/tmp/spark-demo-dist/hop-data/out/countries/ # host + Genovia/.txt + Wakanda/.txt + Latveria/.txt +``` + +- **Three country folders** → Workflow Executor ran the child workflow once per input row. +- **Same marker basename** in all three folders → one parent mini-pipeline/transform instance + processed all rows (common for a tiny CSV on one partition, or **DRIVER_ONLY**). +- **Different marker basenames** → multiple Spark partition instances of Workflow Executor + (**DISTRIBUTED** fan-out). + +### Nested Native Spark pipelines (heavy work per key) + +Same cluster; one **full Native Spark child pipeline** per country (shared parent +`SparkSession`, not a new `spark-submit` app per key): + +```bash +docker compose -f docker/integration-tests/integration-tests-spark-native-cluster.yaml \ + exec spark /opt/hop-samples/spark-demo/scripts/spark-submit-pipelines-demo.sh +``` + +Inspect on the host: + +```bash +find /tmp/spark-demo-dist/hop-data/out/by-country -type f | sort +``` + +Expect directories `Genovia/`, `Wakanda/`, `Latveria/` under `out/by-country/`. Logs should +mention **Nested Native Spark pipeline reusing parent SparkSession**. + +**Force Driver Only** is required on the Pipeline Executor: nested +`SparkPipelineEngine` must start on the driver. The sample HPL sets attribute +`spark` / `run_mode` = `FORCE_DRIVER_ONLY`. Child run configuration is `spark-cluster` +(Native Spark). + +### Generic transform run mode + +On the Native Spark run configuration, **Generic transform run mode** defaults to +`DISTRIBUTED`. To force nested workflows / pipeline executors onto the Spark driver, +set the run config to `DRIVER_ONLY`, or right-click the transform → **Spark Run Mode** → +**Force Driver Only**. + +A 3-row CSV may still use a single partition under `DISTRIBUTED`; folders still prove +package `PROJECT_HOME` resolution and shared-volume side effects. + +Override the dist folder with `DIST_DIR=/somewhere/else` for prepare and the same path as +`HOP_DIST_DIR=…` for compose if needed. The shared data plane defaults to +`${HOP_DIST_DIR}/hop-data` (override with `HOP_DATA_HOST_DIR`). + +Master UI: http://localhost:8080 + +Full narrative, troubleshooting, and design notes: +[Cluster mapping walk-through](../../../../../../docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/cluster-mapping-walkthrough.adoc) +(or the published user manual page *Cluster mapping walk-through* under Native Spark). diff --git a/plugins/engines/spark/src/samples/spark-demo/cluster-env.json b/plugins/engines/spark/src/samples/spark-demo/cluster-env.json new file mode 100644 index 00000000000..f5eb9e95083 --- /dev/null +++ b/plugins/engines/spark/src/samples/spark-demo/cluster-env.json @@ -0,0 +1,14 @@ +{ + "variables" : [ + { + "name" : "HOP_DATA", + "value" : "file:///data/hop-data", + "description" : "Shared Docker volume for Spark Dataset I/O on all workers (not package PROJECT_HOME)" + }, + { + "name" : "EXECUTIONS_INFORMATION_FOLDER", + "value" : "file:///data/hop-data/executions/", + "description" : "Where execution information ends up" + } + ] +} \ No newline at end of file diff --git a/plugins/engines/spark/src/samples/spark-demo/data/countries.csv b/plugins/engines/spark/src/samples/spark-demo/data/countries.csv new file mode 100644 index 00000000000..49619a4b24f --- /dev/null +++ b/plugins/engines/spark/src/samples/spark-demo/data/countries.csv @@ -0,0 +1,4 @@ +country_name +Genovia +Wakanda +Latveria diff --git a/plugins/engines/spark/src/samples/spark-demo/data/customers-sample.csv b/plugins/engines/spark/src/samples/spark-demo/data/customers-sample.csv new file mode 100644 index 00000000000..ece2a25b7a1 --- /dev/null +++ b/plugins/engines/spark/src/samples/spark-demo/data/customers-sample.csv @@ -0,0 +1,6 @@ +id,firstname,lastname +1,Ada,Lovelace +2,Grace,Hopper +3,Alan,Turing +4,Katherine,Johnson +5,Dorothy,Vaughan diff --git a/plugins/engines/spark/src/samples/spark-demo/metadata/execution-data-profile/first-last.json b/plugins/engines/spark/src/samples/spark-demo/metadata/execution-data-profile/first-last.json new file mode 100644 index 00000000000..dc221af4745 --- /dev/null +++ b/plugins/engines/spark/src/samples/spark-demo/metadata/execution-data-profile/first-last.json @@ -0,0 +1,16 @@ +{ + "name": "first-last", + "description": "", + "sampler": [ + { + "FirstRowsExecutionDataSampler": { + "sampleSize": "100" + } + }, + { + "LastRowsExecutionDataSampler": { + "sampleSize": "100" + } + } + ] +} \ No newline at end of file diff --git a/plugins/engines/spark/src/samples/spark-demo/metadata/execution-info-location/data-executions.json b/plugins/engines/spark/src/samples/spark-demo/metadata/execution-info-location/data-executions.json new file mode 100644 index 00000000000..6d7e03d1f99 --- /dev/null +++ b/plugins/engines/spark/src/samples/spark-demo/metadata/execution-info-location/data-executions.json @@ -0,0 +1,14 @@ +{ + "executionInfoLocation": { + "caching-file-location": { + "createParentFolder": true, + "persistenceDelay": "5000", + "rootFolder": "${EXECUTIONS_INFORMATION_FOLDER}", + "maxCacheAge": "86400000" + } + }, + "dataLoggingDelay": "2000", + "name": "data-executions", + "description": "", + "dataLoggingInterval": "5000" +} \ No newline at end of file diff --git a/plugins/engines/spark/src/samples/spark-demo/metadata/pipeline-run-configuration/local.json b/plugins/engines/spark/src/samples/spark-demo/metadata/pipeline-run-configuration/local.json new file mode 100644 index 00000000000..c2ecac5f016 --- /dev/null +++ b/plugins/engines/spark/src/samples/spark-demo/metadata/pipeline-run-configuration/local.json @@ -0,0 +1,22 @@ +{ + "engineRunConfiguration": { + "Local": { + "feedback_size": "50000", + "sample_size": "100", + "sample_type_in_gui": "Last", + "wait_time": "20", + "rowset_size": "10000", + "safe_mode": false, + "show_feedback": false, + "topo_sort": false, + "gather_metrics": false, + "transactional": false + } + }, + "defaultSelection": true, + "configurationVariables": [], + "name": "local", + "description": "Runs your pipelines locally with the standard local Hop pipeline engine", + "dataProfile": "first-last", + "executionInfoLocationName": "data-executions" +} \ No newline at end of file diff --git a/plugins/engines/spark/src/samples/spark-demo/metadata/pipeline-run-configuration/spark-cluster.json b/plugins/engines/spark/src/samples/spark-demo/metadata/pipeline-run-configuration/spark-cluster.json new file mode 100644 index 00000000000..cda0d8db684 --- /dev/null +++ b/plugins/engines/spark/src/samples/spark-demo/metadata/pipeline-run-configuration/spark-cluster.json @@ -0,0 +1,23 @@ +{ + "engineRunConfiguration": { + "SparkPipelineEngine": { + "generic_transform_run_mode": "DISTRIBUTED", + "tempLocation": "/tmp", + "sparkMaster": "", + "driverMemory": "", + "executorMemory": "", + "sparkConfigs": "spark.ui.enabled\u003dfalse\nspark.sql.shuffle.partitions\u003d4", + "executorCores": "", + "sparkAppName": "hop-spark-demo", + "fatJar": "", + "pathSchemeMap": "", + "pluginsToStage": "" + } + }, + "defaultSelection": true, + "configurationVariables": [], + "name": "spark-cluster", + "description": "Native Spark for spark-submit (master blank so CLI --master wins). Use with MainSpark --HopProjectPackage on the Docker cluster walk-through. generic_transform_run_mode: DISTRIBUTED or DRIVER_ONLY.", + "dataProfile": "first-last", + "executionInfoLocationName": "data-executions" +} \ No newline at end of file diff --git a/plugins/engines/spark/src/samples/spark-demo/metadata/workflow-run-configuration/local.json b/plugins/engines/spark/src/samples/spark-demo/metadata/workflow-run-configuration/local.json new file mode 100644 index 00000000000..30330285535 --- /dev/null +++ b/plugins/engines/spark/src/samples/spark-demo/metadata/workflow-run-configuration/local.json @@ -0,0 +1,12 @@ +{ + "engineRunConfiguration": { + "Local": { + "safe_mode": false, + "transactional": false + } + }, + "defaultSelection": true, + "name": "local", + "description": "Runs your workflows locally with the standard local Hop workflow engine", + "executionInfoLocationName": "data-executions" +} \ No newline at end of file diff --git a/plugins/engines/spark/src/samples/spark-demo/pipelines/01-enrich-with-mapping.hpl b/plugins/engines/spark/src/samples/spark-demo/pipelines/01-enrich-with-mapping.hpl new file mode 100644 index 00000000000..8ec178acb74 --- /dev/null +++ b/plugins/engines/spark/src/samples/spark-demo/pipelines/01-enrich-with-mapping.hpl @@ -0,0 +1,207 @@ + + + + + N + 1000 + 100 + Normal + 0 + + 01-enrich-with-mapping + Y + Native Spark demo: File Input → Simple Mapping (child under PROJECT_HOME) → File Output. Data paths use HOP_DATA, not PROJECT_HOME. + - + - + 2026/07/17 00:00:00.000 + 2026/07/17 00:00:00.000 + + + SparkFileInput + customers-sample.csv + ${HOP_DATA}/customers-sample.csv + csv +

Y
+ , + " + N + N + + + + id + Integer + 9 + -1 + + + firstname + String + 50 + -1 + + + lastname + String + 50 + -1 + + + Y + 1 + + 128 + 128 + + Dataset read — use HOP_DATA (shared storage), not package PROJECT_HOME + + none + + + + + + SimpleMapping + mappings/upper-name.hpl + local + + + + + firstname + firstname + + + lastname + lastname + + Y + Y + + + + + + displayName + displayName + + Y + N + + + + Y + + + ${PROJECT_HOME}/pipelines/mappings/upper-name.hpl + Y + 1 + + 416 + 128 + + Child pipeline path is only valid when PROJECT_HOME is the materialised project package on each executor + + none + + + + + + SparkFileOutput + out/enriched - CSV + ${HOP_DATA}/out/enriched + csv + Overwrite +
Y
+ , + " + + 1 + + Y + 1 + + 704 + 128 + + + + none + + + +
+ + Dummy + results + Y + 1 + + 560 + 128 + + + none + + + + + + Dummy + preview + Y + 1 + + 272 + 128 + + + none + + + + + + + mappings/upper-name.hpl + results + Y + + + results + out/enriched - CSV + Y + + + customers-sample.csv + preview + Y + + + preview + mappings/upper-name.hpl + Y + + + + + + diff --git a/plugins/engines/spark/src/samples/spark-demo/pipelines/02-run-workflows.hpl b/plugins/engines/spark/src/samples/spark-demo/pipelines/02-run-workflows.hpl new file mode 100644 index 00000000000..e903b5f1278 --- /dev/null +++ b/plugins/engines/spark/src/samples/spark-demo/pipelines/02-run-workflows.hpl @@ -0,0 +1,165 @@ + + + + + N + 1000 + 100 + Normal + 0 + + 02-run-workflows + Y + Native Spark demo: Spark File Input (countries) → Workflow Executor. Child workflow under PROJECT_HOME creates HOP_DATA/out/countries/${COUNTRY_NAME}/${Internal.Pipeline.ID}.txt markers. + - + - + 2026/07/17 00:00:00.000 + 2026/07/17 00:00:00.000 + + + SparkFileInput + countries.csv + ${HOP_DATA}/countries.csv + csv +
Y
+ , + " + N + N + + + + country_name + String + 100 + -1 + + + Y + 1 + + 128 + 128 + + Read country names — HOP_DATA shared volume, not package PROJECT_HOME + + none + + + +
+ + WorkflowExecutor + create-country-folder.hwf + local + ${PROJECT_HOME}/workflows/create-country-folder.hwf + 1 + + + + + COUNTRY_NAME + country_name + + + + results + ExecutionTime + ExecutionResult + ExecutionNrErrors + ExecutionLinesRead + ExecutionLinesWritten + ExecutionLinesInput + ExecutionLinesOutput + ExecutionLinesRejected + ExecutionLinesUpdated + ExecutionLinesDeleted + ExecutionFilesRetrieved + ExecutionExitStatus + ExecutionLogText + ExecutionLogChannelId + + + FileName + Y + Y + 1 + + 480 + 128 + + Runs create-country-folder.hwf once per input row (group_size=1). Child path needs package PROJECT_HOME on the JVM that runs this transform. + + none + + + + + + Dummy + preview + Y + 1 + + 304 + 128 + + + none + + + + + + Dummy + results + Y + 1 + + 672 + 128 + + + none + + + + + + + countries.csv + preview + Y + + + preview + create-country-folder.hwf + Y + + + create-country-folder.hwf + results + Y + + + + + +
diff --git a/plugins/engines/spark/src/samples/spark-demo/pipelines/03-run-pipelines.hpl b/plugins/engines/spark/src/samples/spark-demo/pipelines/03-run-pipelines.hpl new file mode 100644 index 00000000000..b03a9b95c03 --- /dev/null +++ b/plugins/engines/spark/src/samples/spark-demo/pipelines/03-run-pipelines.hpl @@ -0,0 +1,175 @@ + + + + + N + 1000 + 100 + Normal + 0 + + 03-run-pipelines + Y + Orchestrator: Spark File Input (countries) → Pipeline Executor (Force Driver Only) → nested Native Spark child per country. Child reuses parent SparkSession and writes HOP_DATA/out/by-country/${COUNTRY_NAME}/. + - + - + 2026/07/17 00:00:00.000 + 2026/07/17 00:00:00.000 + + + SparkFileInput + countries.csv + ${HOP_DATA}/countries.csv + csv +
Y
+ , + " + N + N + + + + country_name + String + 100 + -1 + + + Y + 1 + + 128 + 128 + + Control keys — one nested Native Spark job per country + + none + + + +
+ + PipelineExecutor + nested/enrich-by-country.hpl + spark-cluster + N + + 1 + + + + + COUNTRY_NAME + country_name + + + + Y + results + ExecutionTime + ExecutionResult + ExecutionNrErrors + ExecutionLinesRead + ExecutionLinesWritten + ExecutionLinesInput + ExecutionLinesOutput + ExecutionLinesRejected + ExecutionLinesUpdated + ExecutionLinesDeleted + ExecutionFilesRetrieved + ExecutionExitStatus + ExecutionLogText + ExecutionLogChannelId + + + FileName + ${PROJECT_HOME}/pipelines/nested/enrich-by-country.hpl + Y + 1 + + 464 + 128 + + Starts nested Native Spark pipeline per row. Force Driver Only so the child engine runs on the driver and reuses the parent SparkSession. + + none + + + + + spark + + run_mode + FORCE_DRIVER_ONLY + + + + + + Dummy + preview + Y + 1 + + 288 + 128 + + + none + + + + + + Dummy + results + Y + 1 + + 656 + 128 + + + none + + + + + + + countries.csv + preview + Y + + + preview + nested/enrich-by-country.hpl + Y + + + nested/enrich-by-country.hpl + results + Y + + + + + +
diff --git a/plugins/engines/spark/src/samples/spark-demo/pipelines/mappings/upper-name.hpl b/plugins/engines/spark/src/samples/spark-demo/pipelines/mappings/upper-name.hpl new file mode 100644 index 00000000000..1f15fa95932 --- /dev/null +++ b/plugins/engines/spark/src/samples/spark-demo/pipelines/mappings/upper-name.hpl @@ -0,0 +1,128 @@ + + + + + upper-name + Y + Mapping child: build uppercase display name (loaded via ${PROJECT_HOME} on Spark workers) + Normal + + N + - + 2026/07/17 00:00:00.000 + - + 2026/07/17 00:00:00.000 + + + + + Input + build display name + Y + + + build display name + Output + Y + + + + Input + MappingInput + + Y + 1 + + none + + + + + firstname + String + + + + + lastname + String + + + + + + + 128 + 128 + + + + build display name + ScriptValueMod + + Y + 1 + + none + + + 9 + + + 0 + Script 1 + +var displayName = (String(firstname) + " " + String(lastname)).toUpperCase(); + + + + + + displayName + displayName + String + -1 + -1 + N + + + + + 320 + 128 + + + + Output + MappingOutput + + Y + 1 + + none + + + + + 512 + 128 + + + + + diff --git a/plugins/engines/spark/src/samples/spark-demo/pipelines/nested/enrich-by-country.hpl b/plugins/engines/spark/src/samples/spark-demo/pipelines/nested/enrich-by-country.hpl new file mode 100644 index 00000000000..6a040803438 --- /dev/null +++ b/plugins/engines/spark/src/samples/spark-demo/pipelines/nested/enrich-by-country.hpl @@ -0,0 +1,116 @@ + + + + + enrich-by-country + Y + Nested Native Spark child: full Dataset job parameterized by COUNTRY_NAME. Reads shared customers sample and writes under HOP_DATA/out/by-country/${COUNTRY_NAME}/ (proves nested SparkPipelineEngine reuses parent session). + Normal + + + COUNTRY_NAME + Unknown + Country key from the parent Pipeline Executor + + + N + - + 2026/07/17 00:00:00.000 + - + 2026/07/17 00:00:00.000 + + + + + Spark File Input + Spark File Output + Y + + + + SparkFileInput + Spark File Input + Shared fact sample — path uses HOP_DATA, not package PROJECT_HOME + Y + 1 + + none + + + ${HOP_DATA}/customers-sample.csv + csv +
Y
+ , + " + N + N + + + + id + Integer + 9 + -1 + + + firstname + String + 50 + -1 + + + lastname + String + 50 + -1 + + + + + 128 + 128 + +
+ + SparkFileOutput + Spark File Output + Per-country sink — parameter COUNTRY_NAME from parent orchestrator + Y + 1 + + none + + + ${HOP_DATA}/out/by-country/${COUNTRY_NAME} + csv + Overwrite +
Y
+ , + " + + 1 + + + + 384 + 128 + +
+ + +
diff --git a/plugins/engines/spark/src/samples/spark-demo/project-config.json b/plugins/engines/spark/src/samples/spark-demo/project-config.json new file mode 100644 index 00000000000..0787a23f135 --- /dev/null +++ b/plugins/engines/spark/src/samples/spark-demo/project-config.json @@ -0,0 +1,20 @@ +{ + "metadataBaseFolder": "${PROJECT_HOME}/metadata", + "unitTestsBasePath": "${PROJECT_HOME}", + "dataSetsCsvFolder": "${PROJECT_HOME}/datasets", + "enforcingExecutionInHome": true, + "config": { + "variables": [ + { + "name": "HOP_LICENSE_HEADER_FILE", + "value": "${PROJECT_HOME}/../../../../../../integration-tests/asf-header.txt", + "description": "This will automatically serialize the ASF license header into pipelines and workflows in the integration test projects" + }, + { + "name": "HOP_DATA", + "value": "${PROJECT_HOME}/data", + "description": "Local default for data paths. On the Docker cluster, override to file:///data/hop-data via cluster-env.json." + } + ] + } +} diff --git a/plugins/engines/spark/src/samples/spark-demo/scripts/prepare-dist.sh b/plugins/engines/spark/src/samples/spark-demo/scripts/prepare-dist.sh new file mode 100755 index 00000000000..6888e2d1fab --- /dev/null +++ b/plugins/engines/spark/src/samples/spark-demo/scripts/prepare-dist.sh @@ -0,0 +1,117 @@ +#!/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. + +# Prepare artifacts for the Native Spark cluster walk-through (outside the source tree): +# 1) native-provided fat jar +# 2) Spark project package zip (definitions + metadata) +# 3) seed data copy (also copied into the Docker volume by the submit script) +# +# Usage (from Hop install or with HOP_HOME set; repository root optional via REPO_ROOT): +# ./plugins/engines/spark/src/samples/spark-demo/scripts/prepare-dist.sh +# +# Environment: +# HOP_HOME Hop client install (default: current dir if hop-conf.sh is present) +# REPO_ROOT Hop git checkout (default: detected from this script location) +# DIST_DIR Output directory (default: /tmp/spark-demo-dist) +# PROJECT_NAME Hop project name for hop-conf (default: spark-demo) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SAMPLE_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +# scripts → spark-demo → samples → src → spark → engines → plugins → repo root +REPO_ROOT="${REPO_ROOT:-$(cd "${SCRIPT_DIR}/../../../../../../.." && pwd)}" +DIST_DIR="${DIST_DIR:-/tmp/spark-demo-dist}" +PROJECT_NAME="${PROJECT_NAME:-spark-demo}" +FAT_JAR_NAME="${FAT_JAR_NAME:-hop-native-spark4-submit.jar}" +PACKAGE_NAME="${PACKAGE_NAME:-spark-demo.zip}" + +if [[ -n "${HOP_HOME:-}" ]]; then + HOP_CONF="${HOP_HOME}/hop-conf.sh" +elif [[ -x "./hop-conf.sh" ]]; then + HOP_CONF="$(pwd)/hop-conf.sh" + HOP_HOME="$(pwd)" +elif [[ -x "${REPO_ROOT}/assemblies/client/target/hop/hop-conf.sh" ]]; then + HOP_HOME="${REPO_ROOT}/assemblies/client/target/hop" + HOP_CONF="${HOP_HOME}/hop-conf.sh" +else + echo "ERROR: hop-conf.sh not found. Set HOP_HOME to a Hop install that includes the native Spark plugin." >&2 + exit 1 +fi + +if [[ ! -x "${HOP_CONF}" ]]; then + echo "ERROR: not executable: ${HOP_CONF}" >&2 + exit 1 +fi + +mkdir -p "${DIST_DIR}" +echo ">>> HOP_HOME=${HOP_HOME}" +echo ">>> SAMPLE_DIR=${SAMPLE_DIR}" +echo ">>> DIST_DIR=${DIST_DIR}" + +echo ">>> Generating native-provided fat jar (Spark provided by cluster)..." +( + cd "${HOP_HOME}" + ./hop-conf.sh \ + --generate-fat-jar="${DIST_DIR}/${FAT_JAR_NAME}" \ + --spark-client-version=native-provided +) + +# Note: Hop has two different "project" flags: +# -j / (enable for this command) → sets PROJECT_HOME [ProjectsOptionPlugin] +# -p / --project → name for create/delete/list only [ManageProjects] +# You do NOT need to register spark-demo in hop-config for the export below. +# Optional GUI convenience: hop-conf -pc -p spark-demo -ph "${SAMPLE_DIR}" + +echo ">>> Exporting Native Spark project package from ${SAMPLE_DIR}..." +( + cd "${HOP_HOME}" + ./hop-conf.sh \ + --export-spark-project="${DIST_DIR}/${PACKAGE_NAME}" \ + --export-spark-project-home="${SAMPLE_DIR}" +) + +echo ">>> Copying sample data into dist (for inspection; cluster uses /data/hop-data)..." +mkdir -p "${DIST_DIR}/data" +cp -f "${SAMPLE_DIR}/data/customers-sample.csv" "${DIST_DIR}/data/" +cp -f "${SAMPLE_DIR}/data/countries.csv" "${DIST_DIR}/data/" +cp -f "${SAMPLE_DIR}/cluster-env.json" "${DIST_DIR}/" + +# Host bind mount for the cluster data plane (compose: …/hop-data → /data/hop-data) +HOP_DATA_HOST_DIR="${HOP_DATA_HOST_DIR:-${DIST_DIR}/hop-data}" +mkdir -p "${HOP_DATA_HOST_DIR}/out" "${HOP_DATA_HOST_DIR}/executions" "${HOP_DATA_HOST_DIR}/packages" +echo ">>> Host data plane: ${HOP_DATA_HOST_DIR} (mounted as /data/hop-data in the cluster)" + +echo +echo "Done. Contents of ${DIST_DIR}:" +ls -la "${DIST_DIR}" +echo +echo "Next:" +if [[ "${DIST_DIR}" == "/tmp/spark-demo-dist" ]]; then + echo " docker compose -f ${REPO_ROOT}/docker/integration-tests/integration-tests-spark-native-cluster.yaml up -d --build --scale spark-worker=2" +else + echo " HOP_DIST_DIR=${DIST_DIR} HOP_DATA_HOST_DIR=${HOP_DATA_HOST_DIR} \\" + echo " docker compose -f ${REPO_ROOT}/docker/integration-tests/integration-tests-spark-native-cluster.yaml up -d --build --scale spark-worker=2" +fi +echo " docker compose -f ${REPO_ROOT}/docker/integration-tests/integration-tests-spark-native-cluster.yaml exec spark /opt/hop-samples/spark-demo/scripts/spark-submit-demo.sh" +echo " # Workflow Executor country demo:" +echo " docker compose -f ${REPO_ROOT}/docker/integration-tests/integration-tests-spark-native-cluster.yaml exec spark /opt/hop-samples/spark-demo/scripts/spark-submit-workflows-demo.sh" +echo " # Nested Native Spark pipelines (per country) demo:" +echo " docker compose -f ${REPO_ROOT}/docker/integration-tests/integration-tests-spark-native-cluster.yaml exec spark /opt/hop-samples/spark-demo/scripts/spark-submit-pipelines-demo.sh" +echo " # Inspect on the host (outputs + execution info):" +echo " ls -la ${HOP_DATA_HOST_DIR}/out ${HOP_DATA_HOST_DIR}/executions" diff --git a/plugins/engines/spark/src/samples/spark-demo/scripts/spark-submit-demo.sh b/plugins/engines/spark/src/samples/spark-demo/scripts/spark-submit-demo.sh new file mode 100755 index 00000000000..1164800e643 --- /dev/null +++ b/plugins/engines/spark/src/samples/spark-demo/scripts/spark-submit-demo.sh @@ -0,0 +1,176 @@ +#!/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. + +# Run inside the Spark master container (see integration-tests-spark-native-cluster.yaml). +# +# docker compose ... exec spark /opt/hop-samples/spark-demo/scripts/spark-submit-demo.sh +# +# Expects: +# /opt/hop-dist/hop-native-spark4-submit.jar (native-provided fat jar) +# /opt/hop-dist/spark-demo.zip (Native Spark project package) +# /data/hop-data (shared volume on master + workers) +# +# Environment overrides: +# FAT_JAR, PACKAGE_ZIP, RUN_CONFIG, PIPELINE_PATH, SPARK_MASTER, HOP_DATA_DIR, CLUSTER_ENV +# +# Pipelines: +# pipelines/01-enrich-with-mapping.hpl (default) — Simple Mapping +# pipelines/02-run-workflows.hpl — Workflow Executor countries +# pipelines/03-run-pipelines.hpl — nested Native Spark per country + +set -euo pipefail + +SPARK_HOME="${SPARK_HOME:-/spark}" +FAT_JAR="${FAT_JAR:-/opt/hop-dist/hop-native-spark4-submit.jar}" +PACKAGE_ZIP="${PACKAGE_ZIP:-/opt/hop-dist/spark-demo.zip}" +CLUSTER_ENV="${CLUSTER_ENV:-/opt/hop-dist/cluster-env.json}" +SAMPLE_DIR="${SAMPLE_DIR:-/opt/hop-samples/spark-demo}" +SAMPLE_DATA="${SAMPLE_DATA:-${SAMPLE_DIR}/data/customers-sample.csv}" +COUNTRIES_DATA="${COUNTRIES_DATA:-${SAMPLE_DIR}/data/countries.csv}" +HOP_DATA_DIR="${HOP_DATA_DIR:-/data/hop-data}" +SPARK_MASTER_URL="${SPARK_MASTER:-spark://spark:7077}" +RUN_CONFIG="${RUN_CONFIG:-spark-cluster}" +PIPELINE_PATH="${PIPELINE_PATH:-pipelines/01-enrich-with-mapping.hpl}" +DRIVER_HOST="${SPARK_DRIVER_HOST:-spark}" + +echo "=== Hop Native Spark demo submit ===" +echo "SPARK_HOME = ${SPARK_HOME}" +echo "FAT_JAR = ${FAT_JAR}" +echo "PACKAGE_ZIP = ${PACKAGE_ZIP}" +echo "SPARK_MASTER = ${SPARK_MASTER_URL}" +echo "PIPELINE_PATH = ${PIPELINE_PATH}" +echo "RUN_CONFIG = ${RUN_CONFIG}" +echo "HOP_DATA_DIR = ${HOP_DATA_DIR}" +echo "DRIVER_HOST = ${DRIVER_HOST}" + +if [[ ! -f "${FAT_JAR}" ]]; then + echo "ERROR: fat jar not found: ${FAT_JAR}" >&2 + echo "Run prepare-dist.sh on the host and mount HOP_DIST_DIR into /opt/hop-dist." >&2 + exit 1 +fi +if [[ ! -f "${PACKAGE_ZIP}" ]]; then + echo "ERROR: project package not found: ${PACKAGE_ZIP}" >&2 + echo "Export with: hop-conf -j spark-demo --export-spark-project=... " >&2 + exit 1 +fi +if [[ ! -x "${SPARK_HOME}/bin/spark-submit" ]]; then + echo "ERROR: spark-submit not found under ${SPARK_HOME}" >&2 + exit 1 +fi + +# Seed shared data volume (visible to every worker) — data + project package +mkdir -p "${HOP_DATA_DIR}/out" "${HOP_DATA_DIR}/packages" +if [[ -f "${SAMPLE_DATA}" ]]; then + cp -f "${SAMPLE_DATA}" "${HOP_DATA_DIR}/customers-sample.csv" + echo ">>> Seeded ${HOP_DATA_DIR}/customers-sample.csv" +else + echo "WARNING: sample CSV not found at ${SAMPLE_DATA}" >&2 +fi +if [[ -f "${COUNTRIES_DATA}" ]]; then + cp -f "${COUNTRIES_DATA}" "${HOP_DATA_DIR}/countries.csv" + echo ">>> Seeded ${HOP_DATA_DIR}/countries.csv" +else + echo "WARNING: countries CSV not found at ${COUNTRIES_DATA}" >&2 +fi + +# Fresh outputs for control-plane demos +if [[ "${PIPELINE_PATH}" == *"02-run-workflows"* ]]; then + rm -rf "${HOP_DATA_DIR}/out/countries" + mkdir -p "${HOP_DATA_DIR}/out/countries" + echo ">>> Cleared ${HOP_DATA_DIR}/out/countries" +fi +if [[ "${PIPELINE_PATH}" == *"03-run-pipelines"* ]]; then + rm -rf "${HOP_DATA_DIR}/out/by-country" + mkdir -p "${HOP_DATA_DIR}/out/by-country" + echo ">>> Cleared ${HOP_DATA_DIR}/out/by-country" +fi + +# Shared package path: master + workers bind-mount the same host hop-data dir at /data/hop-data, +# so they can open this zip without relying on SparkFiles download (addFile/--files remain as backup). +# Always refresh the shared copy so zip updates under /opt/hop-dist are picked up (Hop re-extracts +# when size/mtime/sha-256 of the package change). +SHARED_PACKAGE="${HOP_DATA_DIR}/packages/$(basename "${PACKAGE_ZIP}")" +mkdir -p "$(dirname "${SHARED_PACKAGE}")" +cp -f "${PACKAGE_ZIP}" "${SHARED_PACKAGE}" +echo ">>> Seeded shared project package ${SHARED_PACKAGE}" +if command -v stat >/dev/null 2>&1; then + echo ">>> Package source: ${PACKAGE_ZIP}" + stat -c '>>> size=%s mtime=%y' "${PACKAGE_ZIP}" 2>/dev/null \ + || stat -f '>>> size=%z mtime=%Sm' "${PACKAGE_ZIP}" 2>/dev/null \ + || true + stat -c '>>> shared size=%s mtime=%y' "${SHARED_PACKAGE}" 2>/dev/null \ + || stat -f '>>> shared size=%z mtime=%Sm' "${SHARED_PACKAGE}" 2>/dev/null \ + || true +fi + +# Optional env file for HOP_DATA=file:///data/hop-data +HOP_CONFIG_ARG=() +if [[ -f "${CLUSTER_ENV}" ]]; then + HOP_CONFIG_ARG=(--HopConfigFile="${CLUSTER_ENV}") + echo ">>> Using cluster env file ${CLUSTER_ENV}" +fi + +echo ">>> spark-submit (client mode; package on shared volume + --files + engine addFile)..." +set -x +"${SPARK_HOME}/bin/spark-submit" \ + --master "${SPARK_MASTER_URL}" \ + --deploy-mode client \ + --class org.apache.hop.spark.run.MainSpark \ + --files "${SHARED_PACKAGE}" \ + --conf "spark.driver.host=${DRIVER_HOST}" \ + --conf "spark.driver.bindAddress=0.0.0.0" \ + --conf "spark.ui.enabled=false" \ + --conf "spark.sql.shuffle.partitions=4" \ + "${FAT_JAR}" \ + --HopProjectPackage="${SHARED_PACKAGE}" \ + --HopPipelinePath="${PIPELINE_PATH}" \ + --HopRunConfigurationName="${RUN_CONFIG}" \ + ${HOP_CONFIG_ARG[@]+"${HOP_CONFIG_ARG[@]}"} +set +x + +echo +echo "=== Submit finished ===" +if [[ "${PIPELINE_PATH}" == *"02-run-workflows"* ]]; then + echo "Country folders + instance markers (shared volume):" + echo " find ${HOP_DATA_DIR}/out/countries -type f | sort" + if [[ -d "${HOP_DATA_DIR}/out/countries" ]]; then + find "${HOP_DATA_DIR}/out/countries" -type f | sort || true + echo "--- unique marker basenames (transform/pipeline instances) ---" + find "${HOP_DATA_DIR}/out/countries" -type f -printf '%f\n' 2>/dev/null | sort -u || true + else + echo "(no out/countries yet — check Spark UI / logs)" + fi +elif [[ "${PIPELINE_PATH}" == *"03-run-pipelines"* ]]; then + echo "Nested Native Spark per-country outputs (shared volume):" + echo " find ${HOP_DATA_DIR}/out/by-country -type f | sort" + if [[ -d "${HOP_DATA_DIR}/out/by-country" ]]; then + find "${HOP_DATA_DIR}/out/by-country" -type f | sort || true + echo "--- per-country dirs ---" + ls -la "${HOP_DATA_DIR}/out/by-country" 2>/dev/null || true + else + echo "(no out/by-country yet — check logs for 'Nested Native Spark pipeline reusing parent SparkSession')" + fi +else + echo "Check mapping output under shared volume:" + echo " ls -la ${HOP_DATA_DIR}/out/enriched || true" + ls -la "${HOP_DATA_DIR}/out/enriched" 2>/dev/null || echo "(output dir not listed yet — check Spark UI / logs)" + if [[ -d "${HOP_DATA_DIR}/out/enriched" ]]; then + echo "--- sample rows ---" + head -n 20 "${HOP_DATA_DIR}/out/enriched"/* 2>/dev/null || true + fi +fi diff --git a/plugins/engines/spark/src/samples/spark-demo/scripts/spark-submit-pipelines-demo.sh b/plugins/engines/spark/src/samples/spark-demo/scripts/spark-submit-pipelines-demo.sh new file mode 100755 index 00000000000..950989251bb --- /dev/null +++ b/plugins/engines/spark/src/samples/spark-demo/scripts/spark-submit-pipelines-demo.sh @@ -0,0 +1,27 @@ +#!/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. + +# Nested Native Spark pipelines orchestrator demo (Pipeline Executor → child Spark jobs). +# +# docker compose ... exec spark /opt/hop-samples/spark-demo/scripts/spark-submit-pipelines-demo.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +export PIPELINE_PATH="${PIPELINE_PATH:-pipelines/03-run-pipelines.hpl}" +exec "${SCRIPT_DIR}/spark-submit-demo.sh" diff --git a/plugins/engines/spark/src/samples/spark-demo/scripts/spark-submit-workflows-demo.sh b/plugins/engines/spark/src/samples/spark-demo/scripts/spark-submit-workflows-demo.sh new file mode 100755 index 00000000000..936268725ac --- /dev/null +++ b/plugins/engines/spark/src/samples/spark-demo/scripts/spark-submit-workflows-demo.sh @@ -0,0 +1,27 @@ +#!/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. + +# Thin wrapper: Workflow Executor country-folder demo on the Docker Spark cluster. +# +# docker compose ... exec spark /opt/hop-samples/spark-demo/scripts/spark-submit-workflows-demo.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +export PIPELINE_PATH="${PIPELINE_PATH:-pipelines/02-run-workflows.hpl}" +exec "${SCRIPT_DIR}/spark-submit-demo.sh" diff --git a/plugins/engines/spark/src/samples/spark-demo/spark-demo-gui-config.json b/plugins/engines/spark/src/samples/spark-demo/spark-demo-gui-config.json new file mode 100644 index 00000000000..8c2b672f108 --- /dev/null +++ b/plugins/engines/spark/src/samples/spark-demo/spark-demo-gui-config.json @@ -0,0 +1,14 @@ +{ + "variables" : [ + { + "name" : "HOP_DATA", + "value" : "/tmp/spark-demo-dist/hop-data", + "description" : "Shared Docker volume for Spark Dataset I/O on all workers (not package PROJECT_HOME)" + }, + { + "name" : "EXECUTIONS_INFORMATION_FOLDER", + "value" : "/tmp/spark-demo-dist/hop-data/executions/", + "description" : "" + } + ] +} \ No newline at end of file diff --git a/plugins/engines/spark/src/samples/spark-demo/workflows/create-country-folder.hwf b/plugins/engines/spark/src/samples/spark-demo/workflows/create-country-folder.hwf new file mode 100644 index 00000000000..d6373b3a81e --- /dev/null +++ b/plugins/engines/spark/src/samples/spark-demo/workflows/create-country-folder.hwf @@ -0,0 +1,138 @@ + + + + create-country-folder + Y + Create ${HOP_DATA}/out/countries/${COUNTRY_NAME} and a marker file named after Internal.Pipeline.ID (parent instance fingerprint when inherit_all_vars is on). + + - + - + 2026/07/17 00:00:00.000 + 2026/07/17 00:00:00.000 + + + + COUNTRY_NAME + Country name from the parent Workflow Executor row + Unknown + + + + + N + 0 + 0 + 60 + 1 + 1 + 0 + 12 + N + Start + + SPECIAL + + 96 + 128 + N + + + + ${HOP_DATA}/out/countries/${COUNTRY_NAME} + N + Create country folder + Shared volume path — use HOP_DATA, not package PROJECT_HOME + CREATE_FOLDER + + 528 + 128 + N + + + + ${HOP_DATA}/out/countries/${COUNTRY_NAME}/${Internal.Pipeline.ID}.txt + N + N + Write instance marker + Marker basename reveals how many parent pipeline/transform instances ran + CREATE_FILE + + 752 + 128 + N + + + + Success + + SUCCESS + + 976 + 128 + N + + + + Basic + Country name + COUNTRY_NAME = "${COUNTRY_NAME}" + Log country + + WRITE_TO_LOG + + 320 + 128 + N + + + + + + Create country folder + Write instance marker + Y + N + Y + + + Write instance marker + Success + Y + N + Y + + + Start + Log country + Y + Y + Y + + + Log country + Create country folder + Y + Y + Y + + + + + diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/core/AcceptFilenamesBindingTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/core/AcceptFilenamesBindingTest.java new file mode 100644 index 00000000000..990ca2cc5f3 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/core/AcceptFilenamesBindingTest.java @@ -0,0 +1,93 @@ +/* + * 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.hop.spark.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.hop.pipeline.transforms.dummy.DummyMeta; +import org.apache.hop.pipeline.transforms.file.BaseFileInput; +import org.apache.hop.pipeline.transforms.file.BaseFileInputMeta; +import org.apache.hop.pipeline.transforms.fileinput.text.TextFileInputMeta; +import org.apache.hop.spark.util.SparkConst; +import org.junit.jupiter.api.Test; + +/** + * Text File Input (and kin) resolve filename sources via {@code + * findInputRowSet(accept_transform_name)} on a main hop — not an INFO stream. The Spark + * mini-pipeline must re-bind that name to {@code _INJECTOR_}. + */ +class AcceptFilenamesBindingTest { + + @Test + void bindRemapsTextFileInputAcceptTransformToMainInjector() { + TextFileInputMeta meta = new TextFileInputMeta(); + meta.getFileInput().setAcceptingFilenames(true); + meta.getFileInput().setAcceptingTransformName("source/test-file*.csv"); + meta.getFileInput().setAcceptingField("filename"); + + assertTrue(HopMapPartitionsFn.bindAcceptingFilenamesToMainInjector(meta)); + assertEquals( + SparkConst.INJECTOR_TRANSFORM_NAME, meta.getFileInput().getAcceptingTransformName()); + } + + @Test + void bindIgnoresNonAcceptingFileInput() { + TextFileInputMeta meta = new TextFileInputMeta(); + meta.getFileInput().setAcceptingFilenames(false); + meta.getFileInput().setAcceptingTransformName("source"); + + assertFalse(HopMapPartitionsFn.bindAcceptingFilenamesToMainInjector(meta)); + assertEquals("source", meta.getFileInput().getAcceptingTransformName()); + } + + @Test + void bindIgnoresUnrelatedTransforms() { + assertFalse(HopMapPartitionsFn.bindAcceptingFilenamesToMainInjector(new DummyMeta())); + assertFalse(HopMapPartitionsFn.bindAcceptingFilenamesToMainInjector(null)); + } + + @Test + void bindWorksForBaseFileInputMetaSubclass() { + BaseFileInput fileInput = new BaseFileInput(); + fileInput.setAcceptingFilenames(true); + fileInput.setAcceptingTransformName("Get File Names"); + BaseFileInputMeta meta = + new BaseFileInputMeta<>() { + @Override + protected BaseFileInput getFileInput() { + return fileInput; + } + + @Override + protected void setFileInput(BaseFileInput input) { + // test double: fixed fileInput instance + } + + @Override + public String getEncoding() { + return null; + } + }; + + assertTrue(HopMapPartitionsFn.bindAcceptingFilenamesToMainInjector(meta)); + assertEquals(SparkConst.INJECTOR_TRANSFORM_NAME, meta.getAcceptingTransformName()); + assertEquals(SparkConst.INJECTOR_TRANSFORM_NAME, fileInput.getAcceptingTransformName()); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/core/HopSparkRowConverterTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/core/HopSparkRowConverterTest.java new file mode 100644 index 00000000000..5c80dea3fd1 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/core/HopSparkRowConverterTest.java @@ -0,0 +1,113 @@ +/* + * 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.hop.spark.core; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.row.RowMeta; +import org.apache.hop.core.row.value.ValueMetaBigNumber; +import org.apache.hop.core.row.value.ValueMetaBinary; +import org.apache.hop.core.row.value.ValueMetaBoolean; +import org.apache.hop.core.row.value.ValueMetaDate; +import org.apache.hop.core.row.value.ValueMetaInteger; +import org.apache.hop.core.row.value.ValueMetaNumber; +import org.apache.hop.core.row.value.ValueMetaString; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.types.DataTypes; +import org.apache.spark.sql.types.StructType; +import org.junit.jupiter.api.Test; + +class HopSparkRowConverterTest { + + @Test + void roundTripBasicTypesIncludingNulls() throws Exception { + IRowMeta rowMeta = new RowMeta(); + rowMeta.addValueMeta(new ValueMetaString("s")); + rowMeta.addValueMeta(new ValueMetaInteger("i")); + rowMeta.addValueMeta(new ValueMetaNumber("n")); + rowMeta.addValueMeta(new ValueMetaBoolean("b")); + rowMeta.addValueMeta(new ValueMetaDate("d")); + rowMeta.addValueMeta(new ValueMetaBigNumber("bn")); + rowMeta.addValueMeta(new ValueMetaBinary("bin")); + + Date now = new Date(1_700_000_000_000L); + Object[] hopRow = + new Object[] { + "hello", + 42L, + 3.5d, + true, + now, + new BigDecimal("123.45"), + "bytes".getBytes(StandardCharsets.UTF_8) + }; + + StructType schema = HopSparkRowConverter.toStructType(rowMeta); + assertEquals(7, schema.fields().length); + assertEquals(DataTypes.StringType, schema.apply("s").dataType()); + assertEquals(DataTypes.LongType, schema.apply("i").dataType()); + assertEquals(DataTypes.DoubleType, schema.apply("n").dataType()); + assertEquals(DataTypes.BooleanType, schema.apply("b").dataType()); + assertEquals(DataTypes.TimestampType, schema.apply("d").dataType()); + assertTrue(HopSparkRowConverter.isDecimal(schema.apply("bn").dataType())); + assertEquals(DataTypes.BinaryType, schema.apply("bin").dataType()); + + Row sparkRow = HopSparkRowConverter.toSparkRow(rowMeta, hopRow); + Object[] back = HopSparkRowConverter.toHopRow(rowMeta, sparkRow); + + assertEquals("hello", back[0]); + assertEquals(42L, back[1]); + assertEquals(3.5d, (Double) back[2], 0.0001); + assertEquals(true, back[3]); + assertEquals(now.getTime(), ((Date) back[4]).getTime()); + assertEquals(0, new BigDecimal("123.45").compareTo((BigDecimal) back[5])); + assertArrayEquals("bytes".getBytes(StandardCharsets.UTF_8), (byte[]) back[6]); + } + + @Test + void toHopRowPadsWhenSparkRowIsShorterThanMeta() throws Exception { + IRowMeta wide = new RowMeta(); + wide.addValueMeta(new ValueMetaString("s")); + wide.addValueMeta(new ValueMetaInteger("i")); + wide.addValueMeta(new ValueMetaString("extra")); + Object[] padded = + HopSparkRowConverter.toHopRow(wide, org.apache.spark.sql.RowFactory.create("only")); + assertEquals("only", padded[0]); + assertNull(padded[1]); + assertNull(padded[2]); + } + + @Test + void nullValuesSurviveRoundTrip() throws Exception { + IRowMeta rowMeta = new RowMeta(); + rowMeta.addValueMeta(new ValueMetaString("s")); + rowMeta.addValueMeta(new ValueMetaInteger("i")); + + Row sparkRow = HopSparkRowConverter.toSparkRow(rowMeta, new Object[] {null, null}); + Object[] back = HopSparkRowConverter.toHopRow(rowMeta, sparkRow); + assertNull(back[0]); + assertNull(back[1]); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/core/SparkNativeMetricsTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/core/SparkNativeMetricsTest.java new file mode 100644 index 00000000000..8d86a95c8b1 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/core/SparkNativeMetricsTest.java @@ -0,0 +1,152 @@ +/* + * 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.hop.spark.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.RowFactory; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.types.DataTypes; +import org.apache.spark.sql.types.StructField; +import org.apache.spark.sql.types.StructType; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class SparkNativeMetricsTest { + + private static SparkSession spark; + + @BeforeAll + static void startSpark() { + spark = + SparkSession.builder() + .appName("hop-spark-native-metrics-test") + .master("local[2]") + .config("spark.ui.enabled", "false") + .config("spark.ui.showConsoleProgress", "false") + .config("spark.metrics.staticSources.enabled", "false") + .config("spark.driver.host", "localhost") + .getOrCreate(); + } + + @AfterAll + static void stopSpark() { + if (spark != null) { + spark.stop(); + } + } + + @Test + void trackReportsInputRoleAcrossPartitions() { + StructType schema = + new StructType( + new StructField[] { + DataTypes.createStructField("id", DataTypes.LongType, false), + }); + List rows = new ArrayList<>(); + for (long i = 0; i < 40; i++) { + rows.add(RowFactory.create(i)); + } + Dataset input = spark.createDataFrame(rows, schema).repartition(4); + + SparkTransformMetricsAccumulator acc = new SparkTransformMetricsAccumulator(); + spark.sparkContext().register(acc, "native-metrics-input"); + + Dataset tracked = + SparkNativeMetrics.track(input, "file-in", acc, SparkNativeMetrics.Role.INPUT); + assertEquals(40L, tracked.count()); + + Map slices = acc.value(); + assertFalse(slices.isEmpty()); + + long totalInput = 0; + long totalWritten = 0; + Set copies = new HashSet<>(); + for (SparkTransformMetricSlice slice : slices.values()) { + assertEquals("file-in", slice.getTransformName()); + assertTrue(slice.isFinished()); + assertTrue(slice.getStartTimeMs() > 0, "partition should record start time"); + assertTrue(slice.getEndTimeMs() >= slice.getStartTimeMs(), "end should be >= start"); + totalInput += slice.getLinesInput(); + totalWritten += slice.getLinesWritten(); + copies.add(slice.getCopyNr()); + } + assertEquals(40L, totalInput); + assertEquals(40L, totalWritten); + assertTrue(copies.size() >= 2, "expected multi-partition copies, got " + copies); + } + + @Test + void trackReportsOutputAndTransformRoles() { + StructType schema = + new StructType( + new StructField[] { + DataTypes.createStructField("v", DataTypes.StringType, false), + }); + List rows = + List.of(RowFactory.create("a"), RowFactory.create("b"), RowFactory.create("c")); + Dataset input = spark.createDataFrame(rows, schema).repartition(2); + + SparkTransformMetricsAccumulator outAcc = new SparkTransformMetricsAccumulator(); + spark.sparkContext().register(outAcc, "native-metrics-output"); + Dataset outTracked = + SparkNativeMetrics.track(input, "file-out", outAcc, SparkNativeMetrics.Role.OUTPUT); + assertEquals(3L, outTracked.count()); + long outSum = + outAcc.value().values().stream().mapToLong(SparkTransformMetricSlice::getLinesOutput).sum(); + long readSum = + outAcc.value().values().stream().mapToLong(SparkTransformMetricSlice::getLinesRead).sum(); + assertEquals(3L, outSum); + assertEquals(3L, readSum); + + SparkTransformMetricsAccumulator txAcc = new SparkTransformMetricsAccumulator(); + spark.sparkContext().register(txAcc, "native-metrics-tx"); + Dataset txTracked = + SparkNativeMetrics.track(input, "sort", txAcc, SparkNativeMetrics.Role.TRANSFORM); + assertEquals(3L, txTracked.count()); + long written = + txAcc.value().values().stream().mapToLong(SparkTransformMetricSlice::getLinesWritten).sum(); + long read = + txAcc.value().values().stream().mapToLong(SparkTransformMetricSlice::getLinesRead).sum(); + assertEquals(3L, written); + assertEquals(3L, read); + } + + @Test + void trackIsNoOpWithoutAccumulator() { + StructType schema = + new StructType( + new StructField[] { + DataTypes.createStructField("v", DataTypes.IntegerType, false), + }); + Dataset input = spark.createDataFrame(List.of(RowFactory.create(1)), schema); + Dataset same = + SparkNativeMetrics.track(input, "x", null, SparkNativeMetrics.Role.TRANSFORM); + assertEquals(input, same); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/core/SparkParallelFileContextTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/core/SparkParallelFileContextTest.java new file mode 100644 index 00000000000..7ae9c78e963 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/core/SparkParallelFileContextTest.java @@ -0,0 +1,86 @@ +/* + * 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.hop.spark.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.hop.core.Const; +import org.apache.hop.core.HopEnvironment; +import org.apache.hop.core.logging.HopLogStore; +import org.apache.hop.core.logging.LoggingObject; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.engines.local.LocalPipelineEngine; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.pipeline.transforms.dummy.DummyMeta; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** Partition-scoped Internal.Transform.* and beamContext for classic I/O on native Spark. */ +class SparkParallelFileContextTest { + + @BeforeAll + static void initHop() throws Exception { + HopEnvironment.init(); + HopLogStore.init(); + } + + @Test + void partitionInternalVariablesUseNameAndPartitionId() { + Variables variables = new Variables(); + HopMapPartitionsFn.applyPartitionInternalVariables(variables, "Text File Output", 3); + + assertEquals("Text File Output", variables.getVariable(Const.INTERNAL_VARIABLE_TRANSFORM_NAME)); + assertEquals("3", variables.getVariable(Const.INTERNAL_VARIABLE_TRANSFORM_COPYNR)); + assertEquals("Text File Output-3", variables.getVariable(Const.INTERNAL_VARIABLE_TRANSFORM_ID)); + assertEquals("3", variables.getVariable(Const.INTERNAL_VARIABLE_TRANSFORM_BUNDLE_NR)); + } + + @Test + void sparkParallelFileContextSetsBeamContextAndOverridesId() throws Exception { + PipelineMeta pipelineMeta = new PipelineMeta(); + pipelineMeta.setName("writer"); + pipelineMeta.setPipelineType(PipelineMeta.PipelineType.SingleThreaded); + TransformMeta dummyTm = new TransformMeta("writer", new DummyMeta()); + dummyTm.setTransformPluginId("Dummy"); + pipelineMeta.addTransform(dummyTm); + + Variables variables = new Variables(); + LocalPipelineEngine pipeline = + new LocalPipelineEngine( + pipelineMeta, variables, new LoggingObject("spark-file-context-test")); + pipeline.prepareExecution(); + + // Simulate init having set UUID-based Internal.Transform.ID + pipeline + .getTransforms() + .get(0) + .transform + .setVariable(Const.INTERNAL_VARIABLE_TRANSFORM_ID, "uuid-xyz"); + + HopMapPartitionsFn.applySparkParallelFileContext(pipeline, "writer", 2); + + var combi = pipeline.getTransforms().get(0); + assertTrue(combi.data.isBeamContext()); + assertEquals(2, combi.data.getBeamBundleNr()); + assertEquals("writer-2", combi.transform.getVariable(Const.INTERNAL_VARIABLE_TRANSFORM_ID)); + assertEquals("2", combi.transform.getVariable(Const.INTERNAL_VARIABLE_TRANSFORM_COPYNR)); + assertEquals("2", combi.transform.getVariable(Const.INTERNAL_VARIABLE_TRANSFORM_BUNDLE_NR)); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/core/SparkTransformMetricsAccumulatorTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/core/SparkTransformMetricsAccumulatorTest.java new file mode 100644 index 00000000000..e122903fea8 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/core/SparkTransformMetricsAccumulatorTest.java @@ -0,0 +1,86 @@ +/* + * 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.hop.spark.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; +import org.junit.jupiter.api.Test; + +class SparkTransformMetricsAccumulatorTest { + + @Test + void addAndMergeKeepsPartitionsSeparateAndTakesMaxCounters() { + SparkTransformMetricsAccumulator acc = new SparkTransformMetricsAccumulator(); + acc.add(slice("Dummy", 0, 100, 100, true, false)); + acc.add(slice("Dummy", 0, 250, 250, true, false)); // progress on same partition + acc.add(slice("Dummy", 1, 80, 80, true, false)); + + Map value = acc.value(); + assertEquals(2, value.size()); + assertEquals(250, value.get(key("Dummy", 0)).getLinesRead()); + assertEquals(250, value.get(key("Dummy", 0)).getLinesWritten()); + assertEquals(80, value.get(key("Dummy", 1)).getLinesRead()); + assertTrue(value.get(key("Dummy", 0)).isRunning()); + assertFalse(value.get(key("Dummy", 0)).isFinished()); + assertEquals(0L, value.get(key("Dummy", 0)).getEndTimeMs()); // still running + + SparkTransformMetricsAccumulator other = new SparkTransformMetricsAccumulator(); + other.add(slice("Dummy", 0, 300, 300, false, true)); + other.add(slice("Filter", 0, 10, 9, false, true)); + acc.merge(other); + + value = acc.value(); + assertEquals(3, value.size()); + assertEquals(300, value.get(key("Dummy", 0)).getLinesRead()); + assertTrue(value.get(key("Dummy", 0)).isFinished()); + assertFalse(value.get(key("Dummy", 0)).isRunning()); + assertEquals(10, value.get(key("Filter", 0)).getLinesRead()); + assertEquals(1_000L, value.get(key("Dummy", 0)).getStartTimeMs()); + assertEquals(1_500L, value.get(key("Dummy", 0)).getEndTimeMs()); + assertEquals(500L, value.get(key("Dummy", 0)).durationMs()); + } + + @Test + void copyAndReset() { + SparkTransformMetricsAccumulator acc = new SparkTransformMetricsAccumulator(); + acc.add(slice("A", 0, 1, 1, true, false)); + assertFalse(acc.isZero()); + + SparkTransformMetricsAccumulator copy = (SparkTransformMetricsAccumulator) acc.copy(); + assertEquals(1, copy.value().size()); + + acc.reset(); + assertTrue(acc.isZero()); + assertEquals(1, copy.value().size()); + } + + private static SparkTransformMetricSlice slice( + String name, int copy, long read, long written, boolean running, boolean finished) { + long start = 1_000L; + long end = finished ? 1_500L : 0L; + return new SparkTransformMetricSlice( + name, copy, "host1", read, written, 0, 0, 0, running, finished, start, end); + } + + private static String key(String name, int copy) { + return name + '\0' + copy; + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/engines/SparkPipelineEngineExecutionInfoTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/engines/SparkPipelineEngineExecutionInfoTest.java new file mode 100644 index 00000000000..272b3382bdc --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/engines/SparkPipelineEngineExecutionInfoTest.java @@ -0,0 +1,136 @@ +/* + * 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.hop.spark.engines; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Date; +import org.apache.hop.core.HopEnvironment; +import org.apache.hop.core.logging.HopLogStore; +import org.apache.hop.core.logging.LogChannel; +import org.apache.hop.execution.ExecutionInfoLocation; +import org.apache.hop.execution.IExecutionInfoLocation; +import org.apache.hop.execution.local.FileExecutionInfoLocation; +import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.config.PipelineRunConfiguration; +import org.apache.hop.pipeline.engine.EngineComponent; +import org.apache.hop.pipeline.engine.EngineComponent.ComponentExecutionStatus; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.pipeline.transforms.dummy.DummyMeta; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Verifies driver-side execution information location wiring for {@link SparkPipelineEngine} + * without running a full Spark job. + */ +class SparkPipelineEngineExecutionInfoTest { + + @TempDir Path tempDir; + + @BeforeAll + static void initHop() throws Exception { + HopEnvironment.init(); + HopLogStore.init(); + } + + @Test + void registerAndUpdateExecutionInfoToFileLocation() throws Exception { + Path root = tempDir.resolve("exec-info"); + Files.createDirectories(root); + + FileExecutionInfoLocation fileLocation = new FileExecutionInfoLocation(root.toString()); + ExecutionInfoLocation locationMeta = + new ExecutionInfoLocation( + "spark-exec", "test location", "100", "200", null, null, fileLocation); + + MemoryMetadataProvider metadata = new MemoryMetadataProvider(); + metadata.getSerializer(ExecutionInfoLocation.class).save(locationMeta); + + SparkPipelineRunConfiguration sparkEngine = new SparkPipelineRunConfiguration(); + sparkEngine.setSparkMaster("local[1]"); + sparkEngine.setSparkAppName("exec-info-test"); + + PipelineRunConfiguration runConfig = new PipelineRunConfiguration(); + runConfig.setName("spark-local"); + runConfig.setEngineRunConfiguration(sparkEngine); + runConfig.setExecutionInfoLocationName("spark-exec"); + metadata.getSerializer(PipelineRunConfiguration.class).save(runConfig); + + PipelineMeta pipelineMeta = new PipelineMeta(); + pipelineMeta.setName("spark-exec-pipe"); + TransformMeta dummy = new TransformMeta("Dummy", new DummyMeta()); + dummy.setTransformPluginId("Dummy"); + pipelineMeta.addTransform(dummy); + + SparkPipelineEngine engine = new SparkPipelineEngine(); + engine.setPipelineMeta(pipelineMeta); + engine.setMetadataProvider(metadata); + engine.setPipelineRunConfiguration(runConfig); + engine.setExecutionStartDate(new Date()); + + // Mimic prepareExecution log channel setup + var logField = SparkPipelineEngine.class.getDeclaredField("logChannel"); + logField.setAccessible(true); + logField.set(engine, new LogChannel(engine)); + + engine.lookupExecutionInformationLocation(); + engine.registerPipelineExecutionInformation(); + + // lookupExecutionInformationLocation clones the metadata location and initialize()s it. + // Use that instance (not the raw pre-init fileLocation) for update/register paths. + var locationField = SparkPipelineEngine.class.getDeclaredField("executionInfoLocation"); + locationField.setAccessible(true); + ExecutionInfoLocation engineLocation = (ExecutionInfoLocation) locationField.get(engine); + assertNotNull(engineLocation, "expected execution info location after lookup"); + IExecutionInfoLocation iLocation = engineLocation.getExecutionInfoLocation(); + assertNotNull(iLocation); + + // Seed components with stable log channel ids + EngineComponent component = new EngineComponent("Dummy", 0); + component.setStatus(ComponentExecutionStatus.STATUS_RUNNING); + component.setLinesRead(10); + component.setLinesWritten(10); + // Use populate path via seed + var seed = SparkPipelineEngine.class.getDeclaredMethod("seedEngineMetricsComponents"); + seed.setAccessible(true); + seed.invoke(engine); + + engine.updatePipelineState(iLocation); + + // Final update + close + engine.stopExecutionInfoTimer(); + + String logId = engine.getLogChannelId(); + assertNotNull(logId); + Path executionFolder = root.resolve(logId); + assertTrue(Files.isDirectory(executionFolder), "expected folder " + executionFolder); + assertTrue(Files.exists(executionFolder.resolve("execution.json"))); + assertTrue(Files.exists(executionFolder.resolve("state.json"))); + + // Second stop is idempotent + engine.stopExecutionInfoTimer(); + assertFalse(Files.list(root).findAny().isEmpty()); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/engines/SparkPipelineEngineMetricsTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/engines/SparkPipelineEngineMetricsTest.java new file mode 100644 index 00000000000..e7660767f2a --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/engines/SparkPipelineEngineMetricsTest.java @@ -0,0 +1,235 @@ +/* + * 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.hop.spark.engines; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.hop.core.HopEnvironment; +import org.apache.hop.core.logging.HopLogStore; +import org.apache.hop.core.logging.LogChannel; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.row.RowMeta; +import org.apache.hop.core.row.value.ValueMetaString; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider; +import org.apache.hop.pipeline.Pipeline; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.engine.EngineComponent.ComponentExecutionStatus; +import org.apache.hop.pipeline.engine.EngineMetrics; +import org.apache.hop.pipeline.engine.IEngineComponent; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.pipeline.transforms.dummy.DummyMeta; +import org.apache.hop.spark.core.SparkTransformMetricSlice; +import org.apache.hop.spark.core.SparkTransformMetricsAccumulator; +import org.apache.hop.spark.pipeline.handler.SparkGenericTransformHandler; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.RowFactory; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.types.DataTypes; +import org.apache.spark.sql.types.StructField; +import org.apache.spark.sql.types.StructType; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Verifies mapPartitions transforms publish per-partition Hop counters into a Spark accumulator, + * and that {@link SparkPipelineEngine#populateEngineMetrics()} maps them to engine components. + */ +class SparkPipelineEngineMetricsTest { + + private static SparkSession spark; + + @BeforeAll + static void startSpark() throws Exception { + HopEnvironment.init(); + HopLogStore.init(); + spark = + SparkSession.builder() + .appName("hop-spark-engine-metrics-test") + .master("local[2]") + .config("spark.ui.enabled", "false") + .config("spark.ui.showConsoleProgress", "false") + .config("spark.metrics.staticSources.enabled", "false") + .config("spark.sql.shuffle.partitions", "4") + .config("spark.driver.host", "localhost") + .getOrCreate(); + } + + @AfterAll + static void stopSpark() { + if (spark != null) { + spark.stop(); + } + } + + @Test + void mapPartitionsReportsPerPartitionMetrics() throws Exception { + StructType schema = + new StructType( + new StructField[] { + DataTypes.createStructField("name", DataTypes.StringType, false), + }); + List rows = new ArrayList<>(); + for (int i = 0; i < 20; i++) { + rows.add(RowFactory.create("r" + i)); + } + // Multiple partitions → multiple engine component copies for the same transform + Dataset input = spark.createDataFrame(rows, schema).repartition(4); + + SparkTransformMetricsAccumulator acc = new SparkTransformMetricsAccumulator(); + spark.sparkContext().register(acc, "hop-metrics-test"); + + DummyMeta dummyMeta = new DummyMeta(); + TransformMeta dummyTm = new TransformMeta("pass", dummyMeta); + dummyTm.setTransformPluginId("Dummy"); + + PipelineMeta pipelineMeta = new PipelineMeta(); + pipelineMeta.addTransform(dummyTm); + + IRowMeta inputMeta = new RowMeta(); + inputMeta.addValueMeta(new ValueMetaString("name")); + + SparkGenericTransformHandler handler = new SparkGenericTransformHandler(); + handler.setMetricsAccumulator(acc); + + Map> map = new HashMap<>(); + handler.handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + new MemoryMetadataProvider(), + "{}", + pipelineMeta, + dummyTm, + map, + spark, + inputMeta, + List.of(), + input); + + long count = map.get("pass").count(); + assertEquals(20L, count); + + Map slices = acc.value(); + assertFalse(slices.isEmpty(), "expected metric slices from mapPartitions"); + + long totalRead = 0; + long totalWritten = 0; + Set copyNrs = new HashSet<>(); + for (SparkTransformMetricSlice slice : slices.values()) { + assertEquals("pass", slice.getTransformName()); + assertTrue(slice.isFinished()); + assertFalse(slice.isRunning()); + totalRead += slice.getLinesRead(); + totalWritten += slice.getLinesWritten(); + copyNrs.add(slice.getCopyNr()); + } + // Dummy reads and writes each row once across all partitions + assertEquals(20L, totalRead); + assertEquals(20L, totalWritten); + assertTrue( + copyNrs.size() >= 2, + "expected multiple partition copies, got copyNrs=" + copyNrs + " slices=" + slices.size()); + } + + @Test + void populateEngineMetricsMapsSlicesToComponents() throws Exception { + SparkPipelineEngine engine = new SparkPipelineEngine(); + PipelineMeta pipelineMeta = new PipelineMeta(); + pipelineMeta.setName("metrics-seed"); + TransformMeta nativeLike = new TransformMeta("file-in", new DummyMeta()); + nativeLike.setTransformPluginId("Dummy"); + TransformMeta mapPart = new TransformMeta("pass", new DummyMeta()); + mapPart.setTransformPluginId("Dummy"); + pipelineMeta.addTransform(nativeLike); + pipelineMeta.addTransform(mapPart); + engine.setPipelineMeta(pipelineMeta); + + long t0 = 1_700_000_000_000L; + SparkTransformMetricsAccumulator acc = new SparkTransformMetricsAccumulator(); + acc.add( + new SparkTransformMetricSlice( + "pass", 0, "host-a", 10, 10, 0, 0, 0, false, true, t0, t0 + 2_000L)); + acc.add( + new SparkTransformMetricSlice( + "pass", 1, "host-b", 5, 5, 0, 0, 0, false, true, t0 + 100L, t0 + 1_100L)); + + // Reflect package-visible wiring via package-private-ish: set through start path is heavy; + // call populate after injecting via setMetrics using reflection-free package access — + // populateEngineMetrics is protected; test is same package. + setMetricsFields(engine, acc); + engine.setFinished(true); + engine.setRunning(false); + engine.setExecutionStartDate(new java.util.Date(t0 - 1000)); + engine.setExecutionEndDate(new java.util.Date(t0 + 3000)); + // status finished + engine.populateEngineMetrics(); + + EngineMetrics metrics = engine.getEngineMetrics(); + assertNotNull(metrics); + List passCopies = engine.getComponentCopies("pass"); + assertEquals(2, passCopies.size()); + + IEngineComponent c0 = engine.findComponent("pass", 0); + IEngineComponent c1 = engine.findComponent("pass", 1); + assertNotNull(c0); + assertNotNull(c1); + assertEquals(10L, c0.getLinesRead()); + assertEquals(5L, c1.getLinesRead()); + assertEquals(ComponentExecutionStatus.STATUS_FINISHED, c0.getStatus()); + + // Duration: GUI uses firstRowReadDate / lastRowWrittenDate per component copy + assertNotNull(c0.getFirstRowReadDate()); + assertNotNull(c0.getLastRowWrittenDate()); + assertEquals(t0, c0.getFirstRowReadDate().getTime()); + assertEquals(t0 + 2_000L, c0.getLastRowWrittenDate().getTime()); + assertEquals(2_000L, c0.getExecutionDuration()); + assertEquals(1_000L, c1.getExecutionDuration()); + + // Native/placeholder transform without slices still listed + IEngineComponent fileIn = engine.findComponent("file-in", 0); + assertNotNull(fileIn); + + Long read0 = metrics.getComponentMetric(c0, Pipeline.METRIC_READ); + assertEquals(10L, read0); + + EngineMetrics filtered = engine.getEngineMetrics("pass", 1); + assertEquals(1, filtered.getComponents().size()); + assertEquals(1, filtered.getComponents().get(0).getCopyNr()); + } + + /** Package-level field injection for unit testing populateEngineMetrics. */ + private static void setMetricsFields( + SparkPipelineEngine engine, SparkTransformMetricsAccumulator acc) throws Exception { + var accField = SparkPipelineEngine.class.getDeclaredField("metricsAccumulator"); + accField.setAccessible(true); + accField.set(engine, acc); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/engines/SparkPipelineRunConfigurationTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/engines/SparkPipelineRunConfigurationTest.java new file mode 100644 index 00000000000..2d68ac43955 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/engines/SparkPipelineRunConfigurationTest.java @@ -0,0 +1,49 @@ +/* + * 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.hop.spark.engines; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; + +import org.apache.hop.spark.util.SparkConst; +import org.junit.jupiter.api.Test; + +class SparkPipelineRunConfigurationTest { + + @Test + void defaultsAndClone() { + SparkPipelineRunConfiguration config = new SparkPipelineRunConfiguration(); + assertEquals(SparkConst.PLUGIN_ID, config.getEnginePluginId()); + assertEquals("local[*]", config.getSparkMaster()); + + config.setSparkMaster("spark://host:7077"); + config.setSparkAppName("test-app"); + config.setDriverMemory("1g"); + config.setPathSchemeMap("s3=s3a\nminio=s3a"); + config.setGenericTransformRunMode("DRIVER_ONLY"); + + SparkPipelineRunConfiguration copy = config.clone(); + assertNotSame(config, copy); + assertEquals("spark://host:7077", copy.getSparkMaster()); + assertEquals("test-app", copy.getSparkAppName()); + assertEquals("1g", copy.getDriverMemory()); + assertEquals("s3=s3a\nminio=s3a", copy.getPathSchemeMap()); + assertEquals("DRIVER_ONLY", copy.getGenericTransformRunMode()); + assertEquals("DISTRIBUTED", new SparkPipelineRunConfiguration().getGenericTransformRunMode()); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/engines/template/SparkRunConfigTemplateTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/engines/template/SparkRunConfigTemplateTest.java new file mode 100644 index 00000000000..698ce103d9b --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/engines/template/SparkRunConfigTemplateTest.java @@ -0,0 +1,96 @@ +/* + * 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.hop.spark.engines.template; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.hop.spark.engines.SparkPipelineRunConfiguration; +import org.junit.jupiter.api.Test; + +class SparkRunConfigTemplateTest { + + @Test + void localExecutionSetsMasterAndUiOff() { + SparkPipelineRunConfiguration config = new SparkPipelineRunConfiguration(); + SparkRunConfigTemplate.LOCAL_EXECUTION.applyTo(config); + assertEquals("local[*]", config.getSparkMaster()); + assertTrue(config.getSparkConfigs().contains("spark.ui.enabled=false")); + assertEquals("", config.getFatJar()); + } + + @Test + void sparkSubmitLeavesMasterBlank() { + SparkPipelineRunConfiguration config = new SparkPipelineRunConfiguration(); + SparkRunConfigTemplate.SPARK_SUBMIT.applyTo(config); + assertEquals("", config.getSparkMaster()); + } + + @Test + void standaloneSetsServerMasterAndMemory() { + SparkPipelineRunConfiguration config = new SparkPipelineRunConfiguration(); + SparkRunConfigTemplate.SPARK_STANDALONE.applyTo(config); + assertEquals("spark://host:7077", config.getSparkMaster()); + assertEquals("2g", config.getDriverMemory()); + assertEquals("2g", config.getExecutorMemory()); + assertEquals("2", config.getExecutorCores()); + } + + @Test + void yarnClientSetsMasterAndDeployMode() { + SparkPipelineRunConfiguration config = new SparkPipelineRunConfiguration(); + SparkRunConfigTemplate.YARN_CLIENT.applyTo(config); + assertEquals("yarn", config.getSparkMaster()); + assertTrue(config.getSparkConfigs().contains("spark.submit.deployMode=client")); + } + + @Test + void fromDisplayNameRoundTrip() { + for (SparkRunConfigTemplate t : SparkRunConfigTemplate.values()) { + assertEquals(t, SparkRunConfigTemplate.fromDisplayName(t.getDisplayName())); + } + assertEquals( + SparkRunConfigTemplate.values().length, SparkRunConfigTemplate.displayNames().length); + } + + @Test + void looksCustomizedDetectsEdits() { + SparkPipelineRunConfiguration fresh = new SparkPipelineRunConfiguration(); + assertFalse(SparkRunConfigTemplate.looksCustomized(fresh)); + + SparkPipelineRunConfiguration edited = new SparkPipelineRunConfiguration(); + edited.setSparkMaster("yarn"); + assertTrue(SparkRunConfigTemplate.looksCustomized(edited)); + + SparkPipelineRunConfiguration withConfigs = new SparkPipelineRunConfiguration(); + withConfigs.setSparkConfigs("spark.ui.enabled=false"); + assertTrue(SparkRunConfigTemplate.looksCustomized(withConfigs)); + } + + @Test + void everyTemplateAppliesWithoutNpe() { + for (SparkRunConfigTemplate t : SparkRunConfigTemplate.values()) { + SparkPipelineRunConfiguration config = new SparkPipelineRunConfiguration(); + t.applyTo(config); + assertNotNull(config.getSparkMaster()); + assertNotNull(t.getDescription()); + } + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/execution/SparkTransformExecutionSamplingTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/execution/SparkTransformExecutionSamplingTest.java new file mode 100644 index 00000000000..a7d8b42b509 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/execution/SparkTransformExecutionSamplingTest.java @@ -0,0 +1,235 @@ +/* + * 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.hop.spark.execution; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import org.apache.hop.core.HopEnvironment; +import org.apache.hop.core.json.HopJson; +import org.apache.hop.core.logging.HopLogStore; +import org.apache.hop.core.logging.LoggingObject; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.row.RowMeta; +import org.apache.hop.core.row.value.ValueMetaString; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.execution.Execution; +import org.apache.hop.execution.ExecutionData; +import org.apache.hop.execution.ExecutionInfoLocation; +import org.apache.hop.execution.ExecutionType; +import org.apache.hop.execution.local.FileExecutionInfoLocation; +import org.apache.hop.execution.profiling.ExecutionDataProfile; +import org.apache.hop.execution.sampler.plugins.first.FirstRowsExecutionDataSampler; +import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.config.PipelineRunConfiguration; +import org.apache.hop.pipeline.engines.local.LocalPipelineEngine; +import org.apache.hop.pipeline.transform.ITransform; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.pipeline.transforms.dummy.DummyMeta; +import org.apache.hop.spark.core.SparkExecutionDataAccumulator; +import org.apache.hop.spark.engines.SparkPipelineRunConfiguration; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class SparkTransformExecutionSamplingTest { + + @TempDir Path tempDir; + + @BeforeAll + static void initHop() throws Exception { + HopEnvironment.init(); + HopLogStore.init(); + } + + @Test + void lookupInactiveWithoutProfile() throws Exception { + MemoryMetadataProvider metadata = new MemoryMetadataProvider(); + FileExecutionInfoLocation fileLocation = + new FileExecutionInfoLocation(tempDir.resolve("loc").toString()); + ExecutionInfoLocation locationMeta = + new ExecutionInfoLocation("loc", "", "100", "200", null, null, fileLocation); + metadata.getSerializer(ExecutionInfoLocation.class).save(locationMeta); + + SparkPipelineRunConfiguration spark = new SparkPipelineRunConfiguration(); + PipelineRunConfiguration runConfig = new PipelineRunConfiguration(); + runConfig.setName("rc"); + runConfig.setEngineRunConfiguration(spark); + runConfig.setExecutionInfoLocationName("loc"); + // no data profile + metadata.getSerializer(PipelineRunConfiguration.class).save(runConfig); + + SparkTransformExecutionSampling sampling = + new SparkTransformExecutionSampling("Dummy", "parent-id", 0); + sampling.lookup(new Variables(), metadata, "rc", "[]"); + assertFalse(sampling.isActive()); + } + + @Test + void sampleAndRegisterData() throws Exception { + Path root = tempDir.resolve("samples"); + Files.createDirectories(root); + + MemoryMetadataProvider metadata = new MemoryMetadataProvider(); + FileExecutionInfoLocation fileLocation = new FileExecutionInfoLocation(root.toString()); + ExecutionInfoLocation locationMeta = + new ExecutionInfoLocation("loc", "", "50", "100", null, null, fileLocation); + metadata.getSerializer(ExecutionInfoLocation.class).save(locationMeta); + + FirstRowsExecutionDataSampler first = new FirstRowsExecutionDataSampler(); + first.setSampleSize("5"); + first.setPluginId("FirstRowsExecutionDataSampler"); + ExecutionDataProfile profile = new ExecutionDataProfile("profile"); + profile.getSamplers().add(first); + metadata.getSerializer(ExecutionDataProfile.class).save(profile); + + SparkPipelineRunConfiguration spark = new SparkPipelineRunConfiguration(); + PipelineRunConfiguration runConfig = new PipelineRunConfiguration(); + runConfig.setName("rc"); + runConfig.setEngineRunConfiguration(spark); + runConfig.setExecutionInfoLocationName("loc"); + runConfig.setExecutionDataProfileName("profile"); + metadata.getSerializer(PipelineRunConfiguration.class).save(runConfig); + + PipelineMeta pipelineMeta = new PipelineMeta(); + pipelineMeta.setName("sample-pipe"); + TransformMeta dummyMeta = new TransformMeta("Dummy", new DummyMeta()); + dummyMeta.setTransformPluginId("Dummy"); + pipelineMeta.addTransform(dummyMeta); + + LocalPipelineEngine pipeline = + new LocalPipelineEngine(pipelineMeta, new Variables(), new LoggingObject("test")); + pipeline.setMetadataProvider(metadata); + pipeline.prepareExecution(); + pipeline.startThreads(); + + IRowMeta rowMeta = new RowMeta(); + rowMeta.addValueMeta(new ValueMetaString("name")); + + // Parent pipeline execution folder must exist before registerData (file location layout) + Execution parentExecution = new Execution(); + parentExecution.setId("parent-log"); + parentExecution.setName("sample-pipe"); + parentExecution.setExecutionType(ExecutionType.Pipeline); + parentExecution.setExecutionStartDate(new java.util.Date()); + fileLocation.initialize(new Variables(), metadata); + fileLocation.registerExecution(parentExecution); + + SparkTransformExecutionSampling sampling = + new SparkTransformExecutionSampling("Dummy", "parent-log", 0); + sampling.lookup(new Variables(), metadata, "rc", "[]"); + assertTrue(sampling.isActive()); + + ITransform transform = pipeline.getTransform("Dummy", 0); + assertNotNull(transform); + sampling.registerExecutingTransform(pipeline); + sampling.attach(new Variables(), pipeline, transform, rowMeta, rowMeta); + + for (int i = 0; i < 3; i++) { + for (var listener : transform.getRowListeners()) { + listener.rowWrittenEvent(rowMeta, new Object[] {"row-" + i}); + } + } + + sampling.sendSamplesToLocation(true); + sampling.close(); + + boolean anyData = + Files.walk(root) + .anyMatch( + p -> + p.getFileName().toString().endsWith("-data.json") + || p.getFileName().toString().equals("execution.json")); + assertTrue(anyData, "expected execution/data artifacts under " + root); + } + + @Test + void samplesAreShippedViaDriverAccumulator() throws Exception { + Path root = tempDir.resolve("acc"); + Files.createDirectories(root); + + MemoryMetadataProvider metadata = new MemoryMetadataProvider(); + FileExecutionInfoLocation fileLocation = new FileExecutionInfoLocation(root.toString()); + ExecutionInfoLocation locationMeta = + new ExecutionInfoLocation("loc", "", "50", "100", null, null, fileLocation); + metadata.getSerializer(ExecutionInfoLocation.class).save(locationMeta); + + FirstRowsExecutionDataSampler first = new FirstRowsExecutionDataSampler(); + first.setSampleSize("5"); + first.setPluginId("FirstRowsExecutionDataSampler"); + ExecutionDataProfile profile = new ExecutionDataProfile("profile"); + profile.getSamplers().add(first); + metadata.getSerializer(ExecutionDataProfile.class).save(profile); + + SparkPipelineRunConfiguration spark = new SparkPipelineRunConfiguration(); + PipelineRunConfiguration runConfig = new PipelineRunConfiguration(); + runConfig.setName("rc"); + runConfig.setEngineRunConfiguration(spark); + runConfig.setExecutionInfoLocationName("loc"); + runConfig.setExecutionDataProfileName("profile"); + metadata.getSerializer(PipelineRunConfiguration.class).save(runConfig); + + PipelineMeta pipelineMeta = new PipelineMeta(); + pipelineMeta.setName("sample-pipe"); + TransformMeta dummyMeta = new TransformMeta("Dummy", new DummyMeta()); + dummyMeta.setTransformPluginId("Dummy"); + pipelineMeta.addTransform(dummyMeta); + + LocalPipelineEngine pipeline = + new LocalPipelineEngine(pipelineMeta, new Variables(), new LoggingObject("test")); + pipeline.setMetadataProvider(metadata); + pipeline.prepareExecution(); + pipeline.startThreads(); + + IRowMeta rowMeta = new RowMeta(); + rowMeta.addValueMeta(new ValueMetaString("name")); + + SparkExecutionDataAccumulator accumulator = new SparkExecutionDataAccumulator(); + SparkTransformExecutionSampling sampling = + new SparkTransformExecutionSampling("Dummy", "parent-log", 0, accumulator); + sampling.lookup(new Variables(), metadata, "rc", "[]"); + assertTrue(sampling.isActive()); + + ITransform transform = pipeline.getTransform("Dummy", 0); + assertNotNull(transform); + sampling.attach(new Variables(), pipeline, transform, rowMeta, rowMeta); + + for (int i = 0; i < 3; i++) { + for (var listener : transform.getRowListeners()) { + listener.rowWrittenEvent(rowMeta, new Object[] {"row-" + i}); + } + } + + sampling.sendSamplesToLocation(true); + sampling.close(); + + Map packed = accumulator.value(); + assertFalse(packed.isEmpty(), "expected sample JSON in driver accumulator"); + ExecutionData data = + HopJson.newMapper().readValue(packed.values().iterator().next(), ExecutionData.class); + assertNotNull(data); + assertTrue(data.isFinished()); + assertNotNull(data.getDataSets()); + assertFalse(data.getDataSets().isEmpty(), "expected sample row buffers after JSON round-trip"); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/metadata/template/SparkCatalogTemplateTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/metadata/template/SparkCatalogTemplateTest.java new file mode 100644 index 00000000000..604e87e4d43 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/metadata/template/SparkCatalogTemplateTest.java @@ -0,0 +1,160 @@ +/* + * 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.hop.spark.metadata.template; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.hop.spark.metadata.SparkCatalog; +import org.apache.hop.spark.table.SparkLakeFormats; +import org.junit.jupiter.api.Test; + +class SparkCatalogTemplateTest { + + @Test + void icebergHadoopLocalSetsWarehouseAndType() { + SparkCatalog cat = new SparkCatalog(); + SparkCatalogTemplate.ICEBERG_HADOOP_LOCAL.applyTo(cat); + assertEquals("lake", cat.getCatalogName()); + assertEquals(SparkCatalog.TYPE_HADOOP, cat.getCatalogType()); + assertEquals(SparkLakeFormats.ICEBERG_CATALOG, cat.getImplementation()); + assertEquals("file:///tmp/hop-warehouse", cat.getWarehouse()); + assertEquals("", cat.getUri()); + assertEquals("", cat.getCredential()); + } + + @Test + void icebergRestSetsUri() { + SparkCatalog cat = new SparkCatalog(); + SparkCatalogTemplate.ICEBERG_REST.applyTo(cat); + assertEquals(SparkCatalog.TYPE_REST, cat.getCatalogType()); + assertEquals("https://catalog.example.com/v1", cat.getUri()); + assertTrue(cat.getWarehouse() == null || cat.getWarehouse().isEmpty()); + } + + @Test + void icebergRestAuthLeavesCredentialEmptyAndDocumentsToken() { + SparkCatalog cat = new SparkCatalog(); + cat.setCredential("should-be-cleared"); + SparkCatalogTemplate.ICEBERG_REST_AUTH.applyTo(cat); + assertEquals(SparkCatalog.TYPE_REST, cat.getCatalogType()); + assertEquals("", cat.getCredential()); + assertTrue(cat.getConfExtra().contains("Credential")); + } + + @Test + void objectStoreSetsS3aAndIoImpl() { + SparkCatalog cat = new SparkCatalog(); + SparkCatalogTemplate.ICEBERG_HADOOP_OBJECT_STORE.applyTo(cat); + assertEquals("s3a://bucket/warehouse", cat.getWarehouse()); + assertTrue(cat.getConfExtra().contains("io-impl=")); + } + + @Test + void hiveAndGlueAreAdvancedTypes() { + SparkCatalog hive = new SparkCatalog(); + SparkCatalogTemplate.HIVE_METASTORE.applyTo(hive); + assertEquals(SparkCatalog.TYPE_HIVE, hive.getCatalogType()); + assertEquals("hive", hive.getCatalogName()); + assertTrue(hive.getConfExtra().contains("thrift://")); + assertTrue(hive.getConfExtra().startsWith("# docs:")); + + SparkCatalog glue = new SparkCatalog(); + SparkCatalogTemplate.AWS_GLUE.applyTo(glue); + assertEquals(SparkCatalog.TYPE_GLUE, glue.getCatalogType()); + assertEquals("glue", glue.getCatalogName()); + assertTrue(glue.getConfExtra().contains("# docs:")); + } + + @Test + void nessieUnityAndDeltaAdvancedTemplates() { + SparkCatalog nessie = new SparkCatalog(); + SparkCatalogTemplate.NESSIE.applyTo(nessie); + assertEquals(SparkCatalog.TYPE_CUSTOM, nessie.getCatalogType()); + assertEquals("nessie", nessie.getCatalogName()); + assertTrue(nessie.getConfExtra().contains("NessieCatalog")); + assertTrue(nessie.getConfExtra().contains(SparkCatalogTemplate.Docs.NESSIE_SPARK)); + + SparkCatalog unity = new SparkCatalog(); + SparkCatalogTemplate.DATABRICKS_UNITY.applyTo(unity); + assertEquals("unity", unity.getCatalogName()); + assertTrue(unity.getConfExtra().contains(SparkCatalogTemplate.Docs.DATABRICKS_UNITY)); + + SparkCatalog delta = new SparkCatalog(); + SparkCatalogTemplate.DELTA_NAMED_CATALOG.applyTo(delta); + assertEquals(SparkLakeFormats.DELTA_CATALOG, delta.getImplementation()); + assertTrue(delta.getConfExtra().contains(SparkCatalogTemplate.Docs.DELTA)); + } + + @Test + void everyTemplateIncludesDocsCommentInConfExtra() { + for (SparkCatalogTemplate t : SparkCatalogTemplate.values()) { + SparkCatalog cat = new SparkCatalog(); + t.applyTo(cat); + assertTrue( + cat.getConfExtra() != null && cat.getConfExtra().contains("# docs:"), + () -> t.name() + " should include # docs: link in conf extra"); + } + } + + @Test + void confWithDocsFormatsHeaderAndBody() { + assertEquals( + "# docs: https://example.com", + SparkCatalogTemplate.confWithDocs("https://example.com", "")); + assertEquals( + "# docs: https://example.com\nuri=thrift://x", + SparkCatalogTemplate.confWithDocs("https://example.com", "uri=thrift://x")); + } + + @Test + void fromDisplayNameRoundTrip() { + for (SparkCatalogTemplate t : SparkCatalogTemplate.values()) { + assertEquals(t, SparkCatalogTemplate.fromDisplayName(t.getDisplayName())); + } + assertEquals(SparkCatalogTemplate.values().length, SparkCatalogTemplate.displayNames().length); + } + + @Test + void looksCustomizedDetectsEdits() { + SparkCatalog fresh = new SparkCatalog(); + assertFalse(SparkCatalogTemplate.looksCustomized(fresh)); + + SparkCatalog withName = new SparkCatalog(); + withName.setCatalogName("lake"); + assertTrue(SparkCatalogTemplate.looksCustomized(withName)); + + SparkCatalog withWarehouse = new SparkCatalog(); + withWarehouse.setWarehouse("file:///tmp/wh"); + assertTrue(SparkCatalogTemplate.looksCustomized(withWarehouse)); + } + + @Test + void everyTemplateAppliesWithoutNpe() { + for (SparkCatalogTemplate t : SparkCatalogTemplate.values()) { + SparkCatalog cat = new SparkCatalog(); + t.applyTo(cat); + assertNotNull(cat.getCatalogType()); + assertNotNull(cat.getImplementation()); + assertNotNull(t.getDescription()); + assertEquals("", cat.getCredential()); + } + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/pipeline/HopPipelineMetaToSparkConverterTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/pipeline/HopPipelineMetaToSparkConverterTest.java new file mode 100644 index 00000000000..8a6a2815e4e --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/pipeline/HopPipelineMetaToSparkConverterTest.java @@ -0,0 +1,183 @@ +/* + * 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.hop.spark.pipeline; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import org.apache.hop.core.annotations.Transform; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.plugins.EngineCompatibility; +import org.apache.hop.core.plugins.EngineCompatibilityResolver; +import org.apache.hop.core.plugins.IPlugin; +import org.apache.hop.pipeline.PipelineHopMeta; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.pipeline.transforms.dummy.DummyMeta; +import org.apache.hop.pipeline.transforms.groupby.GroupByMeta; +import org.apache.hop.spark.engines.SparkPipelineEngine; +import org.apache.hop.spark.util.SparkConst; +import org.junit.jupiter.api.Test; + +class HopPipelineMetaToSparkConverterTest { + + @Test + void sortedGroupByStillBanned() { + HopException group = + assertThrows( + HopException.class, + () -> + HopPipelineMetaToSparkConverter.validateTransformSparkUsage( + SparkConst.GROUP_BY_PLUGIN_ID, "g1")); + assertTrue(group.getMessage().contains("Group By")); + } + + @Test + void nativeHandlersAreNotBanned() throws HopException { + HopPipelineMetaToSparkConverter.validateTransformSparkUsage( + SparkConst.MEMORY_GROUP_BY_PLUGIN_ID, "mg"); + HopPipelineMetaToSparkConverter.validateTransformSparkUsage( + SparkConst.MERGE_JOIN_PLUGIN_ID, "mj"); + HopPipelineMetaToSparkConverter.validateTransformSparkUsage( + SparkConst.UNIQUE_ROWS_PLUGIN_ID, "u"); + HopPipelineMetaToSparkConverter.validateTransformSparkUsage( + SparkConst.SORT_ROWS_PLUGIN_ID, "s"); + HopPipelineMetaToSparkConverter.validateTransformSparkUsage( + SparkConst.SPARK_FILE_INPUT_PLUGIN_ID, "in"); + HopPipelineMetaToSparkConverter.validateTransformSparkUsage( + SparkConst.SPARK_FILE_OUTPUT_PLUGIN_ID, "out"); + HopPipelineMetaToSparkConverter.validateTransformSparkUsage("Calculator", "calc"); + } + + @Test + void supportsSurfacesNativeAndBans() { + SparkPipelineEngine engine = new SparkPipelineEngine(); + + IPlugin banned = mock(IPlugin.class); + when(banned.getIds()).thenReturn(new String[] {SparkConst.GROUP_BY_PLUGIN_ID}); + assertTrue(engine.supports(banned).isUnsupported()); + + IPlugin memoryGroupBy = mock(IPlugin.class); + when(memoryGroupBy.getIds()).thenReturn(new String[] {SparkConst.MEMORY_GROUP_BY_PLUGIN_ID}); + assertTrue(engine.supports(memoryGroupBy).isSupported()); + + IPlugin sort = mock(IPlugin.class); + when(sort.getIds()).thenReturn(new String[] {SparkConst.SORT_ROWS_PLUGIN_ID}); + assertTrue(engine.supports(sort).isSupported()); + + assertEquals(EngineCompatibility.Verdict.UNKNOWN, engine.supports(null).getVerdict()); + } + + @Test + void collectActiveTransformsSkipsDisabledHopsAndDisconnected() { + PipelineMeta pm = new PipelineMeta(); + TransformMeta a = new TransformMeta("A", new DummyMeta()); + a.setTransformPluginId("Dummy"); + TransformMeta b = new TransformMeta("B", new DummyMeta()); + b.setTransformPluginId("Dummy"); + TransformMeta orphan = new TransformMeta("Orphan Sink", new DummyMeta()); + orphan.setTransformPluginId("Dummy"); + pm.addTransform(a); + pm.addTransform(b); + pm.addTransform(orphan); + PipelineHopMeta hop = new PipelineHopMeta(a, b); + hop.setEnabled(false); + pm.addPipelineHop(hop); + + List active = HopPipelineMetaToSparkConverter.collectActiveTransforms(pm); + assertTrue(active.isEmpty(), "disabled hop and disconnected transforms must not run"); + + hop.setEnabled(true); + pm.clearCaches(); + active = HopPipelineMetaToSparkConverter.collectActiveTransforms(pm); + assertEquals(2, active.size()); + assertTrue(active.stream().anyMatch(t -> "A".equals(t.getName()))); + assertTrue(active.stream().anyMatch(t -> "B".equals(t.getName()))); + assertFalse(active.stream().anyMatch(t -> "Orphan Sink".equals(t.getName()))); + } + + @Test + void explicitHandlerSetMatchesRegistrations() { + assertTrue( + HopPipelineMetaToSparkConverter.EXPLICIT_HANDLER_PLUGIN_IDS.contains( + SparkConst.MEMORY_GROUP_BY_PLUGIN_ID)); + assertTrue( + HopPipelineMetaToSparkConverter.EXPLICIT_HANDLER_PLUGIN_IDS.contains( + SparkConst.MERGE_JOIN_PLUGIN_ID)); + assertTrue( + HopPipelineMetaToSparkConverter.EXPLICIT_HANDLER_PLUGIN_IDS.contains( + SparkConst.UNIQUE_ROWS_PLUGIN_ID)); + assertTrue( + HopPipelineMetaToSparkConverter.EXPLICIT_HANDLER_PLUGIN_IDS.contains( + SparkConst.SORT_ROWS_PLUGIN_ID)); + assertTrue( + HopPipelineMetaToSparkConverter.EXPLICIT_HANDLER_PLUGIN_IDS.contains( + SparkConst.SPARK_FILE_INPUT_PLUGIN_ID)); + assertTrue( + HopPipelineMetaToSparkConverter.EXPLICIT_HANDLER_PLUGIN_IDS.contains( + SparkConst.SPARK_FILE_OUTPUT_PLUGIN_ID)); + assertTrue( + HopPipelineMetaToSparkConverter.EXPLICIT_HANDLER_PLUGIN_IDS.contains( + SparkConst.SPARK_LAKE_TABLE_INPUT_PLUGIN_ID)); + assertTrue( + HopPipelineMetaToSparkConverter.EXPLICIT_HANDLER_PLUGIN_IDS.contains( + SparkConst.SPARK_LAKE_TABLE_OUTPUT_PLUGIN_ID)); + assertTrue( + HopPipelineMetaToSparkConverter.EXPLICIT_HANDLER_PLUGIN_IDS.contains( + SparkConst.SPARK_LAKE_TABLE_MERGE_PLUGIN_ID)); + assertTrue( + HopPipelineMetaToSparkConverter.EXPLICIT_HANDLER_PLUGIN_IDS.contains( + SparkConst.SPARK_LAKE_TABLE_MAINTENANCE_PLUGIN_ID)); + } + + /** + * Every plugin id in {@link HopPipelineMetaToSparkConverter#HARD_BANNED_PLUGIN_IDS} must declare + * Native Spark on {@code @Transform.excludedEngines} (value of {@link SparkConst#PLUGIN_ID}). + * Keep {@code bannedMetas} in lockstep when the ban list grows. + */ + @Test + void hardBannedTransformsExcludeNativeSparkOnAnnotation() { + Map> bannedMetas = Map.of(SparkConst.GROUP_BY_PLUGIN_ID, GroupByMeta.class); + + assertEquals( + HopPipelineMetaToSparkConverter.HARD_BANNED_PLUGIN_IDS.keySet(), + bannedMetas.keySet(), + "HARD_BANNED_PLUGIN_IDS and bannedMetas must stay in lockstep"); + + for (Map.Entry> e : bannedMetas.entrySet()) { + Transform ann = e.getValue().getAnnotation(Transform.class); + assertNotNull(ann, e.getKey() + " must have @Transform"); + assertTrue( + EngineCompatibilityResolver.matchesAny(ann.excludedEngines(), SparkConst.PLUGIN_ID), + () -> + e.getKey() + + " @Transform.excludedEngines must include " + + SparkConst.PLUGIN_ID + + " but was " + + Arrays.toString(ann.excludedEngines())); + } + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/pipeline/MultiInputUnionTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/pipeline/MultiInputUnionTest.java new file mode 100644 index 00000000000..60673cce449 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/pipeline/MultiInputUnionTest.java @@ -0,0 +1,237 @@ +/* + * 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.hop.spark.pipeline; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.hop.core.HopEnvironment; +import org.apache.hop.core.logging.HopLogStore; +import org.apache.hop.core.logging.LogChannel; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.pipeline.PipelineHopMeta; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.pipeline.transforms.dummy.DummyMeta; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.RowFactory; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.types.DataTypes; +import org.apache.spark.sql.types.Metadata; +import org.apache.spark.sql.types.StructField; +import org.apache.spark.sql.types.StructType; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** Multi-previous Dataset union (same layout) for native Spark. */ +class MultiInputUnionTest { + + private static SparkSession spark; + + @BeforeAll + static void start() throws Exception { + HopEnvironment.init(); + HopLogStore.init(); + spark = + SparkSession.builder() + .appName("hop-spark-multi-input-test") + .master("local[2]") + .config("spark.ui.enabled", "false") + .config("spark.ui.showConsoleProgress", "false") + .config("spark.metrics.staticSources.enabled", "false") + .getOrCreate(); + } + + @AfterAll + static void stop() { + if (spark != null) { + spark.stop(); + spark = null; + } + } + + @Test + void unionSameLayoutMergesRows() throws Exception { + StructType schema = + new StructType( + new StructField[] { + new StructField("id", DataTypes.LongType, true, Metadata.empty()), + new StructField("name", DataTypes.StringType, true, Metadata.empty()) + }); + Dataset a = + spark.createDataFrame( + List.of(RowFactory.create(1L, "a"), RowFactory.create(2L, "b")), schema); + Dataset b = spark.createDataFrame(List.of(RowFactory.create(3L, "c")), schema); + + PipelineMeta pm = new PipelineMeta(); + TransformMeta left = dummy("left"); + TransformMeta right = dummy("right"); + TransformMeta sink = dummy("sink"); + pm.addTransform(left); + pm.addTransform(right); + pm.addTransform(sink); + pm.addPipelineHop(new PipelineHopMeta(left, sink)); + pm.addPipelineHop(new PipelineHopMeta(right, sink)); + + Map> map = new HashMap<>(); + map.put("left", a); + map.put("right", b); + + // Dummy has no getFields change; attach fields via a spy of getTransformFields is hard — + // use real metas and override by putting identical schemas on both Datasets only. + // getTransformFields on Dummy returns previous fields; with no previous on left/right empty. + // So inject via SelectValues-less approach: call resolve with row meta from Datasets + // indirectly. + // We need pipelineMeta.getTransformFields to return matching metas — Dummy with no previous + // returns empty. Build Constant-less chain: use RowMeta-empty Dummy won't work for check. + // Instead unit-test union path by temporarily using empty layout exclude: Dummy does not + // exclude. Workaround: use pipeline where left/right have injector-like fields via Dummy + // only when we don't check layout (excludeFromRowLayoutVerification) — Dummy returns false. + // + // Practical approach: construct IRowMeta via getTransformFields after adding fields through + // a thin transform is heavy. Validate lookup + union with exclude via StreamLookup exclude? + // StreamLookup excludeFromRowLayoutVerification is true. + // Simplest: only assert layout mismatch throws with real fields from Constant. + + // For merge without layout from Dummy: skip safeMode when both empty. + HopPipelineMetaToSparkConverter.ResolvedInputs resolved = + HopPipelineMetaToSparkConverter.resolveAndUnionInputs( + LogChannel.GENERAL, new Variables(), pm, sink, List.of(left, right), map); + + assertEquals(3, resolved.dataset().count()); + } + + @Test + void layoutMismatchThrows() throws Exception { + StructType schemaA = + new StructType( + new StructField[] { + new StructField("id", DataTypes.LongType, true, Metadata.empty()), + new StructField("name", DataTypes.StringType, true, Metadata.empty()) + }); + StructType schemaB = + new StructType( + new StructField[] { + new StructField("id", DataTypes.LongType, true, Metadata.empty()), + new StructField("other", DataTypes.StringType, true, Metadata.empty()) + }); + Dataset a = spark.createDataFrame(List.of(RowFactory.create(1L, "a")), schemaA); + Dataset b = spark.createDataFrame(List.of(RowFactory.create(2L, "x")), schemaB); + + // Build pipeline with Constant-like field definitions using Dummy won't set fields. + // Use a custom approach: TransformMeta with metas that implement getFields. + PipelineMeta pm = pipelineWithTwoStringFields("left", "right", "sink", "name", "other"); + Map> map = new HashMap<>(); + map.put("left", a); + map.put("right", b); + + Exception ex = + assertThrows( + Exception.class, + () -> + HopPipelineMetaToSparkConverter.resolveAndUnionInputs( + LogChannel.GENERAL, + new Variables(), + pm, + pm.findTransform("sink"), + List.of(pm.findTransform("left"), pm.findTransform("right")), + map)); + assertTrue( + ex.getMessage().contains("same row layout") || ex.getMessage().contains("Mixing"), + () -> "unexpected: " + ex.getMessage()); + } + + @Test + void lookupPrefersTargetStreamKey() { + Map> map = new HashMap<>(); + StructType schema = + new StructType( + new StructField[] {new StructField("id", DataTypes.LongType, true, Metadata.empty())}); + Dataset main = spark.createDataFrame(List.of(RowFactory.create(1L)), schema); + Dataset target = spark.createDataFrame(List.of(RowFactory.create(9L)), schema); + map.put("Filter", main); + map.put("Filter - TARGET - Next", target); + + TransformMeta filter = dummy("Filter"); + TransformMeta next = dummy("Next"); + Dataset found = + HopPipelineMetaToSparkConverter.lookupPreviousDataset( + map, filter, next, LogChannel.GENERAL); + assertEquals(9L, found.collectAsList().get(0).getLong(0)); + } + + private static TransformMeta dummy(String name) { + TransformMeta tm = new TransformMeta(name, new DummyMeta()); + tm.setTransformPluginId("Dummy"); + return tm; + } + + /** + * Two source Dummy transforms are not enough for getFields; attach SelectValues-like row meta by + * using a small anonymous ITransformMeta is heavy. Instead wire Constant fields via Dummy and + * manually call getFields is wrong. Use pipeline with Calculator? Easiest: RowMeta for + * verification via a custom TransformMeta that only implements getFields. + * + *

Here we use Dummy and pre-seed nothing — for layoutMismatch we need getTransformFields to + * return different layouts. Add Injector metas as previous with fixed fields. + */ + private static PipelineMeta pipelineWithTwoStringFields( + String left, String right, String sink, String leftExtra, String rightExtra) + throws Exception { + PipelineMeta pm = new PipelineMeta(); + // Sources with different second field names via Injector + org.apache.hop.pipeline.transforms.injector.InjectorMeta injL = + new org.apache.hop.pipeline.transforms.injector.InjectorMeta(); + injL.getInjectorFields() + .add( + new org.apache.hop.pipeline.transforms.injector.InjectorField( + "id", "Integer", "9", "0")); + injL.getInjectorFields() + .add( + new org.apache.hop.pipeline.transforms.injector.InjectorField( + leftExtra, "String", "50", "-1")); + org.apache.hop.pipeline.transforms.injector.InjectorMeta injR = + new org.apache.hop.pipeline.transforms.injector.InjectorMeta(); + injR.getInjectorFields() + .add( + new org.apache.hop.pipeline.transforms.injector.InjectorField( + "id", "Integer", "9", "0")); + injR.getInjectorFields() + .add( + new org.apache.hop.pipeline.transforms.injector.InjectorField( + rightExtra, "String", "50", "-1")); + + TransformMeta l = new TransformMeta(left, injL); + l.setTransformPluginId("Injector"); + TransformMeta r = new TransformMeta(right, injR); + r.setTransformPluginId("Injector"); + TransformMeta s = dummy(sink); + pm.addTransform(l); + pm.addTransform(r); + pm.addTransform(s); + pm.addPipelineHop(new PipelineHopMeta(l, s)); + pm.addPipelineHop(new PipelineHopMeta(r, s)); + return pm; + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/pipeline/handler/SparkFileIoHandlersTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/pipeline/handler/SparkFileIoHandlersTest.java new file mode 100644 index 00000000000..f879c6a1855 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/pipeline/handler/SparkFileIoHandlersTest.java @@ -0,0 +1,358 @@ +/* + * 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.hop.spark.pipeline.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.hop.core.HopEnvironment; +import org.apache.hop.core.logging.HopLogStore; +import org.apache.hop.core.logging.LogChannel; +import org.apache.hop.core.row.RowMeta; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.core.SparkTransformMetricSlice; +import org.apache.hop.spark.core.SparkTransformMetricsAccumulator; +import org.apache.hop.spark.engines.SparkPipelineRunConfiguration; +import org.apache.hop.spark.transforms.io.SparkField; +import org.apache.hop.spark.transforms.io.SparkFileInputMeta; +import org.apache.hop.spark.transforms.io.SparkFileOutputMeta; +import org.apache.hop.spark.util.SparkConst; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class SparkFileIoHandlersTest { + + private static SparkSession spark; + + @TempDir Path tempDir; + + @BeforeAll + static void start() throws Exception { + HopEnvironment.init(); + HopLogStore.init(); + spark = + SparkSession.builder() + .appName("hop-spark-file-io-test") + .master("local[2]") + .config("spark.ui.enabled", "false") + .config("spark.ui.showConsoleProgress", "false") + .config("spark.metrics.staticSources.enabled", "false") + .config("spark.sql.shuffle.partitions", "2") + .config("spark.driver.host", "localhost") + .getOrCreate(); + } + + @AfterAll + static void stop() { + if (spark != null) { + spark.stop(); + } + } + + @Test + void csvRoundTripWithExplicitSchema() throws Exception { + Path inputFile = tempDir.resolve("people.csv"); + Files.writeString(inputFile, "name,age\nAlice,30\nBob,25\nCarol,40\n", StandardCharsets.UTF_8); + + SparkFileInputMeta inMeta = new SparkFileInputMeta(); + inMeta.setFilePath(inputFile.toString()); + inMeta.setFileFormat(SparkFileInputMeta.FORMAT_CSV); + inMeta.setHeader(true); + inMeta.setSeparator(","); + inMeta.getFields().add(new SparkField("name", "String")); + inMeta.getFields().add(new SparkField("age", "Integer")); + + TransformMeta inTm = new TransformMeta("read", inMeta); + inTm.setTransformPluginId(SparkConst.SPARK_FILE_INPUT_PLUGIN_ID); + PipelineMeta pipelineMeta = new PipelineMeta(); + pipelineMeta.addTransform(inTm); + + SparkTransformMetricsAccumulator metrics = new SparkTransformMetricsAccumulator(); + spark.sparkContext().register(metrics, "file-io-metrics"); + + Map> map = new HashMap<>(); + SparkFileInputHandler inputHandler = new SparkFileInputHandler(); + inputHandler.setMetricsAccumulator(metrics); + inputHandler.handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + new MemoryMetadataProvider(), + "{}", + pipelineMeta, + inTm, + map, + spark, + new RowMeta(), + List.of(), + null); + + Dataset read = map.get("read"); + assertEquals(3, read.count()); + assertEquals(2, read.columns().length); + + long inputRows = + metrics.value().values().stream() + .filter(s -> "read".equals(s.getTransformName())) + .mapToLong(SparkTransformMetricSlice::getLinesInput) + .sum(); + assertEquals(3L, inputRows, "Spark File Input should report linesInput via native metrics"); + + Path outDir = tempDir.resolve("out-csv"); + SparkFileOutputMeta outMeta = new SparkFileOutputMeta(); + outMeta.setFilePath(outDir.toString()); + outMeta.setFileFormat(SparkFileInputMeta.FORMAT_CSV); + outMeta.setSaveMode(SparkFileOutputMeta.MODE_OVERWRITE); + outMeta.setHeader(true); + outMeta.setCoalescePartitions("1"); + + TransformMeta outTm = new TransformMeta("write", outMeta); + outTm.setTransformPluginId(SparkConst.SPARK_FILE_OUTPUT_PLUGIN_ID); + pipelineMeta.addTransform(outTm); + + SparkFileOutputHandler outputHandler = new SparkFileOutputHandler(); + outputHandler.setMetricsAccumulator(metrics); + outputHandler.handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + new MemoryMetadataProvider(), + "{}", + pipelineMeta, + outTm, + map, + spark, + new RowMeta(), + List.of(inTm), + read); + + // Leaf is empty marker; data is on disk + assertEquals(0, map.get("write").count()); + assertTrue(Files.exists(outDir)); + + long outputRows = + metrics.value().values().stream() + .filter(s -> "write".equals(s.getTransformName())) + .mapToLong(SparkTransformMetricSlice::getLinesOutput) + .sum(); + assertEquals(3L, outputRows, "Spark File Output should report linesOutput via native metrics"); + + // Re-read written CSV + long rewritten = + spark + .read() + .option("header", "true") + .option("inferSchema", "true") + .csv(outDir.toString()) + .count(); + assertEquals(3, rewritten); + } + + @Test + void parquetRoundTrip() throws Exception { + Path parquetDir = tempDir.resolve("data.parquet"); + spark.range(0, 10).toDF("id").write().mode("overwrite").parquet(parquetDir.toString()); + + SparkFileInputMeta inMeta = new SparkFileInputMeta(); + inMeta.setFilePath(parquetDir.toString()); + inMeta.setFileFormat(SparkFileInputMeta.FORMAT_PARQUET); + + TransformMeta inTm = new TransformMeta("pq_in", inMeta); + inTm.setTransformPluginId(SparkConst.SPARK_FILE_INPUT_PLUGIN_ID); + PipelineMeta pipelineMeta = new PipelineMeta(); + pipelineMeta.addTransform(inTm); + + Map> map = new HashMap<>(); + new SparkFileInputHandler() + .handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + new MemoryMetadataProvider(), + "{}", + pipelineMeta, + inTm, + map, + spark, + new RowMeta(), + List.of(), + null); + + assertEquals(10, map.get("pq_in").count()); + + Path outParquet = tempDir.resolve("out.parquet"); + SparkFileOutputMeta outMeta = new SparkFileOutputMeta(); + outMeta.setFilePath(outParquet.toString()); + outMeta.setFileFormat(SparkFileInputMeta.FORMAT_PARQUET); + outMeta.setSaveMode(SparkFileOutputMeta.MODE_OVERWRITE); + + TransformMeta outTm = new TransformMeta("pq_out", outMeta); + outTm.setTransformPluginId(SparkConst.SPARK_FILE_OUTPUT_PLUGIN_ID); + pipelineMeta.addTransform(outTm); + + new SparkFileOutputHandler() + .handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + new MemoryMetadataProvider(), + "{}", + pipelineMeta, + outTm, + map, + spark, + new RowMeta(), + List.of(inTm), + map.get("pq_in")); + + assertEquals(10, spark.read().parquet(outParquet.toString()).count()); + } + + /** + * Reproduces customers-1k style issues: semicolon separator, leading spaces on integers, date as + * yyyy/MM/dd, and a field list that omits a middle column (firstname) — values must not shift. + */ + @Test + void csvNameBasedProjectionDoesNotShiftWhenFieldOmitted() throws Exception { + Path inputFile = tempDir.resolve("customers-sample.txt"); + Files.writeString( + inputFile, + "id;name;firstname;zip;city;birthdate;street;housenr;stateCode;state\n" + + " 1;jwcdf-name;fsj-firstname; 13520;oem-city;1954/02/07;amrb-street; 145;AK;ALASKA\n" + + " 2;flhxu-name;tum-firstname; 17520;buo-city;1966/04/24;wfyz-street; 96;GA;GEORGIA\n", + StandardCharsets.UTF_8); + + SparkFileInputMeta inMeta = new SparkFileInputMeta(); + inMeta.setFilePath(inputFile.toString()); + inMeta.setFileFormat(SparkFileInputMeta.FORMAT_CSV); + inMeta.setHeader(true); + inMeta.setSeparator(";"); + // Intentionally omit firstname — must drop that column, not shift the rest + inMeta.getFields().add(new SparkField("id", "Integer")); + inMeta.getFields().add(new SparkField("name", "String")); + inMeta.getFields().add(new SparkField("zip", "String")); + inMeta.getFields().add(new SparkField("city", "String")); + SparkField birth = new SparkField("birthdate", "Date"); + birth.setFormatMask("yyyy/MM/dd"); + inMeta.getFields().add(birth); + inMeta.getFields().add(new SparkField("street", "String")); + inMeta.getFields().add(new SparkField("housenr", "String")); + inMeta.getFields().add(new SparkField("stateCode", "String")); + inMeta.getFields().add(new SparkField("state", "String")); + + TransformMeta inTm = new TransformMeta("customers", inMeta); + inTm.setTransformPluginId(SparkConst.SPARK_FILE_INPUT_PLUGIN_ID); + PipelineMeta pipelineMeta = new PipelineMeta(); + pipelineMeta.addTransform(inTm); + + Map> map = new HashMap<>(); + new SparkFileInputHandler() + .handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + new MemoryMetadataProvider(), + "{}", + pipelineMeta, + inTm, + map, + spark, + new RowMeta(), + List.of(), + null); + + Dataset ds = map.get("customers"); + assertEquals(2, ds.count()); + String[] cols = ds.columns(); + assertEquals(9, cols.length); + assertEquals("id", cols[0]); + assertEquals("name", cols[1]); + assertEquals("zip", cols[2]); + assertEquals("city", cols[3]); + assertEquals("birthdate", cols[4]); + assertEquals("state", cols[8]); + + Row first = ds.orderBy("id").collectAsList().get(0); + assertEquals(1L, first.getLong(0)); + assertEquals("jwcdf-name", first.getString(1)); + assertEquals("13520", first.getString(2)); // zip, not firstname + assertEquals("oem-city", first.getString(3)); + assertTrue(first.get(4) != null, "birthdate should parse"); + assertEquals("amrb-street", first.getString(5)); + assertEquals("145", first.getString(6)); + assertEquals("AK", first.getString(7)); + assertEquals("ALASKA", first.getString(8)); + } + + @Test + void csvInferSchemaWithoutFields() throws Exception { + Path inputFile = tempDir.resolve("nums.csv"); + Files.writeString(inputFile, "a,b\n1,2\n3,4\n", StandardCharsets.UTF_8); + + SparkFileInputMeta inMeta = new SparkFileInputMeta(); + inMeta.setFilePath(inputFile.toString()); + inMeta.setFileFormat(SparkFileInputMeta.FORMAT_CSV); + inMeta.setHeader(true); + inMeta.setInferSchema(true); + + TransformMeta inTm = new TransformMeta("infer", inMeta); + inTm.setTransformPluginId(SparkConst.SPARK_FILE_INPUT_PLUGIN_ID); + PipelineMeta pipelineMeta = new PipelineMeta(); + pipelineMeta.addTransform(inTm); + + Map> map = new HashMap<>(); + new SparkFileInputHandler() + .handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + new MemoryMetadataProvider(), + "{}", + pipelineMeta, + inTm, + map, + spark, + new RowMeta(), + List.of(), + null); + + Dataset ds = map.get("infer"); + assertEquals(2, ds.count()); + assertEquals(2, ds.columns().length); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/pipeline/handler/SparkInfoStreamTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/pipeline/handler/SparkInfoStreamTest.java new file mode 100644 index 00000000000..50a3291b31d --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/pipeline/handler/SparkInfoStreamTest.java @@ -0,0 +1,143 @@ +/* + * 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.hop.spark.pipeline.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.apache.hop.core.HopEnvironment; +import org.apache.hop.core.logging.HopLogStore; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.row.RowMeta; +import org.apache.hop.core.row.value.ValueMetaInteger; +import org.apache.hop.core.row.value.ValueMetaString; +import org.apache.hop.pipeline.PipelineHopMeta; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.pipeline.transforms.dummy.DummyMeta; +import org.apache.hop.pipeline.transforms.streamlookup.StreamLookupMeta; +import org.apache.hop.pipeline.transforms.streamlookup.StreamLookupMeta.Lookup; +import org.apache.hop.pipeline.transforms.streamlookup.StreamLookupMeta.MatchKey; +import org.apache.hop.pipeline.transforms.streamlookup.StreamLookupMeta.ReturnValue; +import org.apache.hop.spark.core.SparkInfoStreamSupport; +import org.apache.spark.broadcast.Broadcast; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.RowFactory; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.types.DataTypes; +import org.apache.spark.sql.types.Metadata; +import org.apache.spark.sql.types.StructField; +import org.apache.spark.sql.types.StructType; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for info/side-stream discovery and broadcast helper. End-to-end Stream Lookup is + * covered by integration-tests/spark-native. + */ +class SparkInfoStreamTest { + + private static SparkSession spark; + + @BeforeAll + static void startSpark() throws Exception { + HopEnvironment.init(); + HopLogStore.init(); + spark = + SparkSession.builder() + .appName("hop-spark-info-stream-test") + .master("local[2]") + .config("spark.ui.enabled", "false") + .config("spark.ui.showConsoleProgress", "false") + .config("spark.metrics.staticSources.enabled", "false") + .getOrCreate(); + } + + @AfterAll + static void stopSpark() { + if (spark != null) { + spark.stop(); + spark = null; + } + } + + @Test + void resolveInfoTransformsExcludesMainPrevious() { + PipelineMeta pm = new PipelineMeta(); + TransformMeta mainSrc = new TransformMeta("mainSrc", new DummyMeta()); + mainSrc.setTransformPluginId("Dummy"); + TransformMeta infoSrc = new TransformMeta("infoSrc", new DummyMeta()); + infoSrc.setTransformPluginId("Dummy"); + StreamLookupMeta slMeta = new StreamLookupMeta(); + slMeta.setSourceTransformName("infoSrc"); + Lookup lookup = new Lookup(); + MatchKey mk = new MatchKey(); + mk.setKeyStream("k"); + mk.setKeyLookup("k"); + lookup.getMatchKeys().add(mk); + ReturnValue rv = new ReturnValue(); + rv.setValue("v"); + rv.setValueName("v"); + lookup.getReturnValues().add(rv); + slMeta.setLookup(lookup); + TransformMeta lookupT = new TransformMeta("lookup", slMeta); + lookupT.setTransformPluginId("StreamLookup"); + pm.addTransform(mainSrc); + pm.addTransform(infoSrc); + pm.addTransform(lookupT); + pm.addPipelineHop(new PipelineHopMeta(mainSrc, lookupT)); + pm.addPipelineHop(new PipelineHopMeta(infoSrc, lookupT)); + slMeta.searchInfoAndTargetTransforms(pm.getTransforms()); + + List mainPrev = pm.findPreviousTransforms(lookupT, false); + List info = + SparkGenericTransformHandler.resolveInfoTransforms(pm, lookupT, mainPrev); + + assertEquals(1, mainPrev.size()); + assertEquals("mainSrc", mainPrev.get(0).getName()); + assertEquals(1, info.size()); + assertEquals("infoSrc", info.get(0).getName()); + } + + @Test + void broadcastInfoRowsCollectsHopRows() throws Exception { + StructType schema = + new StructType( + new StructField[] { + new StructField("stateCode", DataTypes.StringType, true, Metadata.empty()), + new StructField("countPerState", DataTypes.LongType, true, Metadata.empty()) + }); + Dataset infoDs = + spark.createDataFrame( + List.of(RowFactory.create("AK", 10L), RowFactory.create("GA", 20L)), schema); + + IRowMeta rowMeta = new RowMeta(); + rowMeta.addValueMeta(new ValueMetaString("stateCode")); + rowMeta.addValueMeta(new ValueMetaInteger("countPerState")); + + Broadcast> broadcast = + SparkInfoStreamSupport.broadcastInfoRows(spark, infoDs, rowMeta, "infoSrc"); + List rows = broadcast.value(); + assertEquals(2, rows.size()); + assertEquals("AK", rows.get(0)[0]); + assertTrue(((Number) rows.get(0)[1]).longValue() == 10L); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/pipeline/handler/SparkNativeHandlersTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/pipeline/handler/SparkNativeHandlersTest.java new file mode 100644 index 00000000000..00958fbc1b6 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/pipeline/handler/SparkNativeHandlersTest.java @@ -0,0 +1,354 @@ +/* + * 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.hop.spark.pipeline.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.hop.core.HopEnvironment; +import org.apache.hop.core.logging.HopLogStore; +import org.apache.hop.core.logging.LogChannel; +import org.apache.hop.core.row.IRowMeta; +import org.apache.hop.core.row.RowMeta; +import org.apache.hop.core.row.value.ValueMetaInteger; +import org.apache.hop.core.row.value.ValueMetaString; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider; +import org.apache.hop.pipeline.PipelineHopMeta; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.pipeline.transforms.memgroupby.GAggregate; +import org.apache.hop.pipeline.transforms.memgroupby.GGroup; +import org.apache.hop.pipeline.transforms.memgroupby.MemoryGroupByMeta; +import org.apache.hop.pipeline.transforms.memgroupby.MemoryGroupByMeta.GroupType; +import org.apache.hop.pipeline.transforms.mergejoin.MergeJoinMeta; +import org.apache.hop.pipeline.transforms.sort.SortRowsField; +import org.apache.hop.pipeline.transforms.sort.SortRowsMeta; +import org.apache.hop.pipeline.transforms.uniquerows.UniqueField; +import org.apache.hop.pipeline.transforms.uniquerows.UniqueRowsMeta; +import org.apache.hop.spark.engines.SparkPipelineRunConfiguration; +import org.apache.hop.spark.util.SparkConst; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.RowFactory; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.types.DataTypes; +import org.apache.spark.sql.types.StructField; +import org.apache.spark.sql.types.StructType; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Integration-style unit tests for native shuffle-aware handlers on a local Spark session. These + * prove global semantics across partitions (not partition-local Hop mini-pipelines). + */ +class SparkNativeHandlersTest { + + private static SparkSession spark; + + @BeforeAll + static void startSpark() throws Exception { + HopEnvironment.init(); + HopLogStore.init(); + spark = + SparkSession.builder() + .appName("hop-spark-native-handlers-test") + .master("local[2]") + .config("spark.ui.enabled", "false") + .config("spark.ui.showConsoleProgress", "false") + .config("spark.metrics.staticSources.enabled", "false") + .config("spark.sql.shuffle.partitions", "4") + .config("spark.driver.host", "localhost") + .getOrCreate(); + } + + @AfterAll + static void stopSpark() { + if (spark != null) { + spark.stop(); + } + } + + @Test + void memoryGroupBySumsAcrossPartitions() throws Exception { + StructType schema = + new StructType( + new StructField[] { + DataTypes.createStructField("category", DataTypes.StringType, false), + DataTypes.createStructField("amount", DataTypes.LongType, false) + }); + List rows = + Arrays.asList( + RowFactory.create("A", 10L), + RowFactory.create("A", 5L), + RowFactory.create("B", 7L), + RowFactory.create("B", 3L), + RowFactory.create("A", 1L)); + // Force multiple partitions so a partition-local group-by would be wrong + Dataset input = spark.createDataFrame(rows, schema).repartition(4); + + MemoryGroupByMeta meta = new MemoryGroupByMeta(); + meta.getGroups().add(new GGroup("category")); + meta.getAggregates().add(new GAggregate("total", "amount", GroupType.Sum, null)); + + TransformMeta groupMeta = new TransformMeta("group", meta); + groupMeta.setTransformPluginId(SparkConst.MEMORY_GROUP_BY_PLUGIN_ID); + + PipelineMeta pipelineMeta = new PipelineMeta(); + pipelineMeta.addTransform(groupMeta); + + Map> map = new HashMap<>(); + IRowMeta inputMeta = new RowMeta(); + inputMeta.addValueMeta(new ValueMetaString("category")); + inputMeta.addValueMeta(new ValueMetaInteger("amount")); + + new SparkMemoryGroupByHandler() + .handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + new MemoryMetadataProvider(), + "{}", + pipelineMeta, + groupMeta, + map, + spark, + inputMeta, + List.of(), + input); + + List result = map.get("group").collectAsList(); + assertEquals(2, result.size()); + Map totals = new HashMap<>(); + for (Row r : result) { + totals.put(r.getString(0), r.getLong(1)); + } + assertEquals(16L, totals.get("A")); + assertEquals(10L, totals.get("B")); + } + + @Test + void mergeJoinInnerAcrossPartitions() throws Exception { + StructType leftSchema = + new StructType( + new StructField[] { + DataTypes.createStructField("id", DataTypes.LongType, false), + DataTypes.createStructField("name", DataTypes.StringType, false) + }); + StructType rightSchema = + new StructType( + new StructField[] { + DataTypes.createStructField("id", DataTypes.LongType, false), + DataTypes.createStructField("city", DataTypes.StringType, false) + }); + + Dataset left = + spark + .createDataFrame( + Arrays.asList( + RowFactory.create(1L, "Alice"), + RowFactory.create(2L, "Bob"), + RowFactory.create(3L, "Carol")), + leftSchema) + .repartition(3); + Dataset right = + spark + .createDataFrame( + Arrays.asList( + RowFactory.create(1L, "NYC"), + RowFactory.create(2L, "LA"), + RowFactory.create(4L, "Chicago")), + rightSchema) + .repartition(3); + + MergeJoinMeta meta = new MergeJoinMeta(); + meta.setJoinType("INNER"); + meta.setLeftTransformName("left"); + meta.setRightTransformName("right"); + meta.getKeyFields1().add("id"); + meta.getKeyFields2().add("id"); + + TransformMeta joinTm = new TransformMeta("join", meta); + joinTm.setTransformPluginId(SparkConst.MERGE_JOIN_PLUGIN_ID); + + TransformMeta leftTm = + new TransformMeta("left", new org.apache.hop.pipeline.transforms.dummy.DummyMeta()); + TransformMeta rightTm = + new TransformMeta("right", new org.apache.hop.pipeline.transforms.dummy.DummyMeta()); + + PipelineMeta pipelineMeta = new PipelineMeta(); + pipelineMeta.addTransform(leftTm); + pipelineMeta.addTransform(rightTm); + pipelineMeta.addTransform(joinTm); + pipelineMeta.addPipelineHop(new PipelineHopMeta(leftTm, joinTm)); + pipelineMeta.addPipelineHop(new PipelineHopMeta(rightTm, joinTm)); + + Map> map = new HashMap<>(); + map.put("left", left); + map.put("right", right); + + new SparkMergeJoinHandler() + .handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + new MemoryMetadataProvider(), + "{}", + pipelineMeta, + joinTm, + map, + spark, + new RowMeta(), + List.of(leftTm, rightTm), + null); + + List result = map.get("join").collectAsList(); + assertEquals(2, result.size()); + // columns: id, name, id_1 (or city with renamed id), city + assertTrue(result.stream().anyMatch(r -> "Alice".equals(r.getString(1)))); + assertTrue(result.stream().anyMatch(r -> "Bob".equals(r.getString(1)))); + } + + @Test + void uniqueRowsGlobalDistinct() throws Exception { + StructType schema = + new StructType( + new StructField[] { + DataTypes.createStructField("code", DataTypes.StringType, false), + DataTypes.createStructField("n", DataTypes.LongType, false) + }); + Dataset input = + spark + .createDataFrame( + Arrays.asList( + RowFactory.create("X", 1L), + RowFactory.create("Y", 2L), + RowFactory.create("X", 3L), + RowFactory.create("Z", 4L), + RowFactory.create("Y", 5L)), + schema) + .repartition(4); + + UniqueRowsMeta meta = new UniqueRowsMeta(); + meta.getCompareFields().add(new UniqueField("code", false)); + + TransformMeta uniqueTm = new TransformMeta("unique", meta); + uniqueTm.setTransformPluginId(SparkConst.UNIQUE_ROWS_PLUGIN_ID); + PipelineMeta pipelineMeta = new PipelineMeta(); + pipelineMeta.addTransform(uniqueTm); + + Map> map = new HashMap<>(); + IRowMeta inputMeta = new RowMeta(); + inputMeta.addValueMeta(new ValueMetaString("code")); + inputMeta.addValueMeta(new ValueMetaInteger("n")); + + new SparkUniqueRowsHandler() + .handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + new MemoryMetadataProvider(), + "{}", + pipelineMeta, + uniqueTm, + map, + spark, + inputMeta, + List.of(), + input); + + List result = map.get("unique").collectAsList(); + assertEquals(3, result.size()); + } + + @Test + void sortRowsGlobalOrder() throws Exception { + StructType schema = + new StructType( + new StructField[] { + DataTypes.createStructField("v", DataTypes.LongType, false), + }); + Dataset input = + spark + .createDataFrame( + Arrays.asList( + RowFactory.create(5L), + RowFactory.create(1L), + RowFactory.create(9L), + RowFactory.create(3L)), + schema) + .repartition(3); + + SortRowsMeta meta = new SortRowsMeta(); + SortRowsField field = new SortRowsField(); + field.setFieldName("v"); + field.setAscending(true); + meta.getSortFields().add(field); + + TransformMeta sortTm = new TransformMeta("sort", meta); + sortTm.setTransformPluginId(SparkConst.SORT_ROWS_PLUGIN_ID); + PipelineMeta pipelineMeta = new PipelineMeta(); + pipelineMeta.addTransform(sortTm); + + Map> map = new HashMap<>(); + IRowMeta inputMeta = new RowMeta(); + inputMeta.addValueMeta(new ValueMetaInteger("v")); + + new SparkSortRowsHandler() + .handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + new MemoryMetadataProvider(), + "{}", + pipelineMeta, + sortTm, + map, + spark, + inputMeta, + List.of(), + input); + + // orderBy is a narrow transformation; collect may not preserve global order without an action + // that respects ordering — takeOrdered is reliable for assertion + List ordered = map.get("sort").sort(colAsc("v")).collectAsList(); + assertEquals(Arrays.asList(1L, 3L, 5L, 9L), extractLongs(ordered)); + } + + private static org.apache.spark.sql.Column colAsc(String name) { + return org.apache.spark.sql.functions.col(name).asc(); + } + + private static List extractLongs(List rows) { + List values = new ArrayList<>(); + for (Row r : rows) { + values.add(r.getLong(0)); + } + return values; + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/pipeline/handler/SparkTargetStreamTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/pipeline/handler/SparkTargetStreamTest.java new file mode 100644 index 00000000000..8ea18a51347 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/pipeline/handler/SparkTargetStreamTest.java @@ -0,0 +1,103 @@ +/* + * 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.hop.spark.pipeline.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.apache.hop.core.HopEnvironment; +import org.apache.hop.core.logging.HopLogStore; +import org.apache.hop.pipeline.PipelineHopMeta; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.pipeline.transforms.dummy.DummyMeta; +import org.apache.hop.pipeline.transforms.filterrows.FilterRowsMeta; +import org.apache.hop.spark.core.HopSparkUtil; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** Unit tests for target-stream discovery and Dataset map key helpers. */ +class SparkTargetStreamTest { + + @BeforeAll + static void initHop() throws Exception { + HopEnvironment.init(); + HopLogStore.init(); + } + + @Test + void createTargetTupleIdMatchesBeamFormat() { + assertEquals( + "Filter - TARGET - True branch", HopSparkUtil.createTargetTupleId("Filter", "True branch")); + } + + @Test + void resolveTargetTransformNamesFromFilterRows() { + PipelineMeta pm = new PipelineMeta(); + TransformMeta src = new TransformMeta("src", new DummyMeta()); + src.setTransformPluginId("Dummy"); + TransformMeta trueT = new TransformMeta("True branch", new DummyMeta()); + trueT.setTransformPluginId("Dummy"); + TransformMeta falseT = new TransformMeta("False branch", new DummyMeta()); + falseT.setTransformPluginId("Dummy"); + + FilterRowsMeta filterMeta = new FilterRowsMeta(); + filterMeta.setTrueTransformName("True branch"); + filterMeta.setFalseTransformName("False branch"); + TransformMeta filter = new TransformMeta("Filter", filterMeta); + filter.setTransformPluginId("FilterRows"); + + pm.addTransform(src); + pm.addTransform(trueT); + pm.addTransform(falseT); + pm.addTransform(filter); + pm.addPipelineHop(new PipelineHopMeta(src, filter)); + pm.addPipelineHop(new PipelineHopMeta(filter, trueT)); + pm.addPipelineHop(new PipelineHopMeta(filter, falseT)); + filterMeta.searchInfoAndTargetTransforms(pm.getTransforms()); + + List targets = SparkGenericTransformHandler.resolveTargetTransformNames(filter); + assertEquals(2, targets.size()); + assertTrue(targets.contains("True branch")); + assertTrue(targets.contains("False branch")); + + List next = pm.findNextTransforms(filter); + assertEquals(2, next.size()); + } + + @Test + void resolveTargetNamesEmptyWhenNotConfigured() { + FilterRowsMeta filterMeta = new FilterRowsMeta(); + TransformMeta filter = new TransformMeta("Filter", filterMeta); + filter.setTransformPluginId("FilterRows"); + List targets = SparkGenericTransformHandler.resolveTargetTransformNames(filter); + assertTrue(targets.isEmpty()); + } + + @Test + void sameFieldLayoutDetectsMismatch() { + org.apache.hop.core.row.RowMeta a = new org.apache.hop.core.row.RowMeta(); + a.addValueMeta(new org.apache.hop.core.row.value.ValueMetaString("country_name")); + org.apache.hop.core.row.RowMeta b = new org.apache.hop.core.row.RowMeta(); + b.addValueMeta(new org.apache.hop.core.row.value.ValueMetaInteger("ExecutionTime")); + b.addValueMeta(new org.apache.hop.core.row.value.ValueMetaBoolean("ExecutionResult")); + assertTrue(SparkGenericTransformHandler.sameFieldLayout(a, a)); + assertTrue(!SparkGenericTransformHandler.sameFieldLayout(a, b)); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/pkg/PackageExportFilterTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/pkg/PackageExportFilterTest.java new file mode 100644 index 00000000000..cabb429164e --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/pkg/PackageExportFilterTest.java @@ -0,0 +1,125 @@ +/* + * 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.hop.spark.pkg; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class PackageExportFilterTest { + + @TempDir File tempDir; + + @Test + void defaultsSkipWorkAndDatasets() { + PackageExportFilter f = PackageExportFilter.empty(); + assertTrue(f.shouldSkipRelative("work/fat.jar")); + assertTrue(f.shouldSkipRelative("datasets/big.csv")); + assertFalse(f.shouldSkipRelative("pipelines/a.hpl")); + assertFalse(f.shouldSkipRelative("mappings/child.hpl")); + } + + @Test + void includePathsOverrideWorkSkip() { + PackageExportFilter f = + new PackageExportFilter(List.of(), List.of(), List.of("work/cluster-env.json"), false); + assertFalse(f.shouldSkipRelative("work/cluster-env.json")); + assertTrue(f.shouldSkipRelative("work/hop-native.jar")); + } + + @Test + void excludeGlobsSkipJars() { + PackageExportFilter f = + new PackageExportFilter(List.of(), List.of("**/*.jar"), List.of(), false); + assertTrue(f.shouldSkipRelative("lib/extra.jar")); + assertFalse(f.shouldSkipRelative("lib/readme.txt")); + } + + @Test + void extraExcludeDirsMerged() { + PackageExportFilter f = + new PackageExportFilter(List.of("tmp", "screenshots"), List.of(), List.of(), false); + assertTrue(f.shouldSkipRelative("tmp/x")); + assertTrue(f.shouldSkipRelative("screenshots/a.png")); + assertTrue(f.shouldSkipRelative("work/j.jar")); // still default + } + + @Test + void replaceDefaultsDropsWorkUnlessListed() { + PackageExportFilter f = + new PackageExportFilter(List.of("datasets"), List.of(), List.of(), true); + assertFalse(f.shouldSkipRelative("work/fat.jar")); + assertTrue(f.shouldSkipRelative("datasets/a.csv")); + } + + @Test + void loadSaveRoundTripAndExportRespectsConfig() throws Exception { + File projectHome = new File(tempDir, "proj"); + assertTrue(projectHome.mkdirs()); + File pipelines = new File(projectHome, "pipelines"); + assertTrue(pipelines.mkdirs()); + Files.writeString(new File(pipelines, "p.hpl").toPath(), "

", StandardCharsets.UTF_8); + File tmp = new File(projectHome, "tmp"); + assertTrue(tmp.mkdirs()); + Files.writeString(new File(tmp, "x.txt").toPath(), "nope", StandardCharsets.UTF_8); + File work = new File(projectHome, "work"); + assertTrue(work.mkdirs()); + Files.writeString(new File(work, "keep-me.json").toPath(), "{}", StandardCharsets.UTF_8); + Files.writeString(new File(work, "skip.jar").toPath(), "jar", StandardCharsets.UTF_8); + + PackageExportFilter cfg = + new PackageExportFilter(List.of("tmp"), List.of(), List.of("work/keep-me.json"), false); + PackageExportFilter.saveToProjectHome(projectHome.getAbsolutePath(), cfg); + assertTrue(new File(projectHome, PackageExportFilter.CONFIG_FILENAME).isFile()); + + File zip = new File(tempDir, "out.zip"); + SparkProjectPackage.exportProject( + projectHome.getAbsolutePath(), + zip.getAbsolutePath(), + new MemoryMetadataProvider(), + new Variables()); + + SparkProjectPackage.Materialized m = SparkProjectPackage.materialize(zip.getAbsolutePath()); + assertTrue(new File(m.projectHome(), "pipelines/p.hpl").isFile()); + assertFalse(new File(m.projectHome(), "tmp/x.txt").exists()); + assertTrue(new File(m.projectHome(), "work/keep-me.json").isFile()); + assertFalse(new File(m.projectHome(), "work/skip.jar").exists()); + } + + @Test + void mergeCliOnTopOfFile() { + PackageExportFilter file = + new PackageExportFilter(List.of("tmp"), List.of("**/*.csv"), List.of(), false); + PackageExportFilter cli = + new PackageExportFilter(List.of("screenshots"), List.of("**/*.zip"), List.of("a"), false); + PackageExportFilter m = PackageExportFilter.empty().merge(file).merge(cli); + assertTrue(m.getExcludeDirs().contains("tmp")); + assertTrue(m.getExcludeDirs().contains("screenshots")); + assertTrue(m.getExcludeGlobs().contains("**/*.csv")); + assertTrue(m.getExcludeGlobs().contains("**/*.zip")); + assertTrue(m.getIncludePaths().contains("a")); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/pkg/SparkProjectPackageTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/pkg/SparkProjectPackageTest.java new file mode 100644 index 00000000000..0e99921f232 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/pkg/SparkProjectPackageTest.java @@ -0,0 +1,379 @@ +/* + * 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.hop.spark.pkg; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import org.apache.hop.core.metadata.SerializableMetadataProvider; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class SparkProjectPackageTest { + + @TempDir File tempDir; + + @BeforeEach + @AfterEach + void clearCache() { + SparkProjectPackage.clearMaterializationCache(); + } + + @Test + void exportAndMaterializeSetsProjectHomeAndMetadata() throws Exception { + File projectHome = new File(tempDir, "my-project"); + assertTrue(projectHome.mkdirs()); + File pipelines = new File(projectHome, "pipelines"); + assertTrue(pipelines.mkdirs()); + File parent = new File(pipelines, "parent.hpl"); + Files.writeString(parent.toPath(), "", StandardCharsets.UTF_8); + File mapping = new File(projectHome, "mappings"); + assertTrue(mapping.mkdirs()); + Files.writeString( + new File(mapping, "child.hpl").toPath(), "", StandardCharsets.UTF_8); + // datasets/ and work/ (fat jars) should be skipped + File datasets = new File(projectHome, "datasets"); + assertTrue(datasets.mkdirs()); + Files.writeString(new File(datasets, "big.csv").toPath(), "a,b\n", StandardCharsets.UTF_8); + File work = new File(projectHome, "work"); + assertTrue(work.mkdirs()); + Files.writeString( + new File(work, "hop-native-spark4-submit.jar").toPath(), + "fake-jar", + StandardCharsets.UTF_8); + + File zip = new File(tempDir, "pkg.zip"); + MemoryMetadataProvider memory = new MemoryMetadataProvider(); + SparkProjectPackage.exportProject( + projectHome.getAbsolutePath(), zip.getAbsolutePath(), memory, new Variables()); + + assertTrue(zip.isFile()); + + SparkProjectPackage.Materialized m = SparkProjectPackage.materialize(zip.getAbsolutePath()); + assertNotNull(m.projectHome()); + assertTrue(new File(m.projectHome(), "pipelines/parent.hpl").isFile()); + assertTrue(new File(m.projectHome(), "mappings/child.hpl").isFile()); + assertTrue(!new File(m.projectHome(), "datasets/big.csv").exists()); + assertTrue(!new File(m.projectHome(), "work/hop-native-spark4-submit.jar").exists()); + assertNotNull(m.metadataPath()); + assertTrue(new File(m.metadataPath()).isFile()); + + Variables vars = new Variables(); + SparkProjectPackage.applyToVariables(vars, m, zip.getAbsolutePath()); + assertEquals(m.projectHome(), vars.getVariable("PROJECT_HOME")); + assertEquals(zip.getAbsolutePath(), vars.getVariable(SparkProjectPackage.VAR_PACKAGE_URI)); + + String resolved = SparkProjectPackage.resolvePipelinePath("pipelines/parent.hpl", m, vars); + assertTrue(resolved.endsWith("parent.hpl")); + assertTrue(new File(resolved).isFile()); + } + + @Test + void ensureMaterializedOnWorkerIsIdempotent() throws Exception { + File projectHome = new File(tempDir, "p2"); + assertTrue(projectHome.mkdirs()); + Files.writeString(new File(projectHome, "a.hpl").toPath(), "

", StandardCharsets.UTF_8); + File zip = new File(tempDir, "p2.zip"); + SparkProjectPackage.exportProject( + projectHome.getAbsolutePath(), + zip.getAbsolutePath(), + new MemoryMetadataProvider(), + new Variables()); + + Variables vars = new Variables(); + vars.setVariable(SparkProjectPackage.VAR_PACKAGE_URI, zip.getAbsolutePath()); + SparkProjectPackage.ensureMaterializedOnWorker(vars); + String home1 = vars.getVariable("PROJECT_HOME"); + SparkProjectPackage.ensureMaterializedOnWorker(vars); + assertEquals(home1, vars.getVariable("PROJECT_HOME")); + } + + @Test + void materializeReExtractsWhenZipContentChanges() throws Exception { + File projectHome = new File(tempDir, "p-change"); + assertTrue(projectHome.mkdirs()); + File pipelines = new File(projectHome, "pipelines"); + assertTrue(pipelines.mkdirs()); + Files.writeString( + new File(pipelines, "v1.hpl").toPath(), "", StandardCharsets.UTF_8); + File zip = new File(tempDir, "change.zip"); + SparkProjectPackage.exportProject( + projectHome.getAbsolutePath(), + zip.getAbsolutePath(), + new MemoryMetadataProvider(), + new Variables()); + + SparkProjectPackage.Materialized m1 = SparkProjectPackage.materialize(zip.getAbsolutePath()); + assertTrue(new File(m1.projectHome(), "pipelines/v1.hpl").isFile()); + assertFalse(new File(m1.projectHome(), "pipelines/v2.hpl").exists()); + String fp1 = SparkProjectPackage.cachedFingerprintForTest(zip.getAbsolutePath()); + assertNotNull(fp1); + assertTrue(fp1.contains("sha256=")); + + // Overwrite the same zip path with new project content (same path, new fingerprint) + Files.writeString( + new File(pipelines, "v2.hpl").toPath(), "", StandardCharsets.UTF_8); + // Ensure mtime differs even on coarse filesystems + Thread.sleep(10); + SparkProjectPackage.exportProject( + projectHome.getAbsolutePath(), + zip.getAbsolutePath(), + new MemoryMetadataProvider(), + new Variables()); + + SparkProjectPackage.Materialized m2 = SparkProjectPackage.materialize(zip.getAbsolutePath()); + assertTrue(new File(m2.projectHome(), "pipelines/v2.hpl").isFile()); + String fp2 = SparkProjectPackage.cachedFingerprintForTest(zip.getAbsolutePath()); + assertNotNull(fp2); + assertFalse(fp1.equals(fp2), "fingerprint must change when zip content changes"); + } + + @Test + void projectHomeDataPathWarning() { + assertNull(SparkProjectPackage.projectHomeDataPathWarning("s3a://b/k")); + assertNotNull(SparkProjectPackage.projectHomeDataPathWarning("${PROJECT_HOME}/files/x.csv")); + } + + @Test + void metadataRoundTripInPackage() throws Exception { + File projectHome = new File(tempDir, "meta-proj"); + assertTrue(projectHome.mkdirs()); + Files.writeString(new File(projectHome, "x.hpl").toPath(), "

", StandardCharsets.UTF_8); + File zip = new File(tempDir, "meta.zip"); + MemoryMetadataProvider memory = new MemoryMetadataProvider(); + SparkProjectPackage.exportProject( + projectHome.getAbsolutePath(), zip.getAbsolutePath(), memory, new Variables()); + SparkProjectPackage.Materialized m = SparkProjectPackage.materialize(zip.getAbsolutePath()); + String json = Files.readString(new File(m.metadataPath()).toPath()); + // parseable metadata JSON + new SerializableMetadataProvider(json); + } + + @Test + void isSparkFilesLocalPathDetectsUserFiles() { + assertTrue( + SparkProjectPackage.isSparkFilesLocalPath("/tmp/spark-abc/userFiles-xyz/spark-demo.zip")); + assertFalse( + SparkProjectPackage.isSparkFilesLocalPath("/data/hop-data/packages/spark-demo.zip")); + } + + @Test + void isClusterSharedPackagePathDetectsVolumesAndObjectStore() { + assertTrue( + SparkProjectPackage.isClusterSharedPackagePath( + "/Volumes/apache-hop/default/jars/hop-spark-package.zip")); + assertTrue(SparkProjectPackage.isClusterSharedPackagePath("/dbfs/FileStore/pkg.zip")); + assertTrue(SparkProjectPackage.isClusterSharedPackagePath("dbfs:/FileStore/pkg.zip")); + assertTrue(SparkProjectPackage.isClusterSharedPackagePath("s3a://bucket/pkg.zip")); + assertFalse(SparkProjectPackage.isClusterSharedPackagePath("/tmp/local-pkg.zip")); + assertFalse(SparkProjectPackage.isClusterSharedPackagePath("file:/tmp/local-pkg.zip")); + assertFalse(SparkProjectPackage.isClusterSharedPackagePath(null)); + } + + @Test + void resolvePackagePathAlwaysReturnsVolumesUriEvenIfNotLocalFile() { + // Executors must not require File.isFile() on /Volumes before materialize (driver-only + // PROJECT_HOME is discarded after re-extract from the shared Volume path). + Variables vars = new Variables(); + vars.setVariable( + SparkProjectPackage.VAR_PACKAGE_URI, + "/Volumes/apache-hop/default/jars/hop-spark-package.zip"); + vars.setVariable("PROJECT_HOME", "/local_disk0/tmp/hop-spark-pkg-deadbeef/project"); + assertEquals( + "/Volumes/apache-hop/default/jars/hop-spark-package.zip", + SparkProjectPackage.resolvePackagePathForMaterialize(vars)); + assertEquals( + java.util.List.of("/Volumes/apache-hop/default/jars/hop-spark-package.zip"), + SparkProjectPackage.listPackagePathsForMaterialize(vars)); + } + + @Test + void resolveFallsBackToClusterSharedWhenSparkFilesMissing() { + // Basename set but SparkFiles.get has nothing → still return cluster-shared URI. + Variables vars = new Variables(); + vars.setVariable(SparkProjectPackage.VAR_PACKAGE_URI, "s3a://bucket/hop-spark-package.zip"); + vars.setVariable(SparkProjectPackage.VAR_PACKAGE_SPARK_FILE, "hop-spark-package.zip"); + assertEquals( + "s3a://bucket/hop-spark-package.zip", + SparkProjectPackage.resolvePackagePathForMaterialize(vars)); + } + + @Test + void ensureMaterializedClearsDriverOnlyProjectHome() throws Exception { + File projectHome = new File(tempDir, "stale-ph"); + assertTrue(projectHome.mkdirs()); + Files.writeString(new File(projectHome, "a.hpl").toPath(), "

", StandardCharsets.UTF_8); + File zip = new File(tempDir, "stale-ph.zip"); + SparkProjectPackage.exportProject( + projectHome.getAbsolutePath(), + zip.getAbsolutePath(), + new MemoryMetadataProvider(), + new Variables()); + + Variables vars = new Variables(); + vars.setVariable(SparkProjectPackage.VAR_PACKAGE_URI, zip.getAbsolutePath()); + // Simulate driver extract path broadcast to an executor that never had that directory + vars.setVariable("PROJECT_HOME", "/local_disk0/tmp/hop-spark-pkg-deadbeef/project"); + SparkProjectPackage.ensureMaterializedOnWorker(vars); + assertTrue(new File(vars.getVariable("PROJECT_HOME")).isDirectory()); + assertTrue(new File(vars.getVariable("PROJECT_HOME"), "a.hpl").isFile()); + assertNotEquals( + "/local_disk0/tmp/hop-spark-pkg-deadbeef/project", vars.getVariable("PROJECT_HOME")); + } + + @Test + void resolvePrefersHopDataPackagesOverMissingSparkFiles() throws Exception { + File projectHome = new File(tempDir, "shared-proj"); + assertTrue(projectHome.mkdirs()); + Files.writeString(new File(projectHome, "a.hpl").toPath(), "

", StandardCharsets.UTF_8); + File zip = new File(tempDir, "shared-pkg.zip"); + SparkProjectPackage.exportProject( + projectHome.getAbsolutePath(), + zip.getAbsolutePath(), + new MemoryMetadataProvider(), + new Variables()); + + File hopData = new File(tempDir, "hop-data"); + File packages = new File(hopData, "packages"); + assertTrue(packages.mkdirs()); + File sharedZip = new File(packages, "shared-pkg.zip"); + Files.copy(zip.toPath(), sharedZip.toPath()); + + Variables vars = new Variables(); + vars.setVariable("HOP_DATA", hopData.getAbsolutePath()); + vars.setVariable(SparkProjectPackage.VAR_PACKAGE_SPARK_FILE, "shared-pkg.zip"); + // Deliberately point URI at a non-existent driver-only path + vars.setVariable(SparkProjectPackage.VAR_PACKAGE_URI, "/nonexistent/shared-pkg.zip"); + + String resolved = SparkProjectPackage.resolvePackagePathForMaterialize(vars); + assertEquals(sharedZip.getAbsolutePath(), resolved); + } + + @Test + void openPackageStreamReadsLocalZip() throws Exception { + File projectHome = new File(tempDir, "stream-proj"); + assertTrue(projectHome.mkdirs()); + Files.writeString(new File(projectHome, "x.hpl").toPath(), "

", StandardCharsets.UTF_8); + File zip = new File(tempDir, "stream.zip"); + SparkProjectPackage.exportProject( + projectHome.getAbsolutePath(), + zip.getAbsolutePath(), + new MemoryMetadataProvider(), + new Variables()); + assertNotNull(SparkProjectPackage.resolveLocalPackageFile(zip.getAbsolutePath())); + try (InputStream in = SparkProjectPackage.openPackageStream(zip.getAbsolutePath())) { + assertTrue(in.read() >= 0); + } + } + + @Test + void ensureLocalPackageFileReturnsExistingFile() throws Exception { + File projectHome = new File(tempDir, "local-src"); + assertTrue(projectHome.mkdirs()); + Files.writeString(new File(projectHome, "z.hpl").toPath(), "

", StandardCharsets.UTF_8); + File zip = new File(tempDir, "local.zip"); + SparkProjectPackage.exportProject( + projectHome.getAbsolutePath(), + zip.getAbsolutePath(), + new MemoryMetadataProvider(), + new Variables()); + String local = SparkProjectPackage.ensureLocalPackageFile(zip.getAbsolutePath()); + assertEquals(zip.getAbsolutePath(), local); + } + + @Test + void stagePackageToLocalTempCopiesReadableZip() throws Exception { + File projectHome = new File(tempDir, "stage-src"); + assertTrue(projectHome.mkdirs()); + Files.writeString(new File(projectHome, "z.hpl").toPath(), "

", StandardCharsets.UTF_8); + File zip = new File(tempDir, "stage.zip"); + SparkProjectPackage.exportProject( + projectHome.getAbsolutePath(), + zip.getAbsolutePath(), + new MemoryMetadataProvider(), + new Variables()); + String staged = SparkProjectPackage.stagePackageToLocalTemp(zip.getAbsolutePath()); + assertNotEquals(zip.getAbsolutePath(), staged); + assertTrue(staged.contains("hop-spark-pkg-src-")); + File stagedFile = new File(staged); + assertTrue(stagedFile.isFile()); + assertTrue(stagedFile.length() > 0); + // second call reuses same staging path when content unchanged + assertEquals(staged, SparkProjectPackage.stagePackageToLocalTemp(zip.getAbsolutePath())); + } + + @Test + void ensureLocalPackageFileStagesDbfsStyleUriViaCopy() throws Exception { + // dbfs: is cluster-shared — must not return a Volume-style path to addFile. + // We cannot open a real dbfs mount in unit tests; use stagePackageToLocalTemp on a real zip + // and assert isClusterSharedPackagePath treats dbfs as shared. + assertTrue(SparkProjectPackage.isClusterSharedPackagePath("dbfs:/FileStore/pkg.zip")); + assertTrue( + SparkProjectPackage.isClusterSharedPackagePath( + "/Volumes/apache-hop/default/jars/hop-spark-package.zip")); + } + + @Test + void distributeToClusterWithLocalSparkSession() throws Exception { + File projectHome = new File(tempDir, "dist-proj"); + assertTrue(projectHome.mkdirs()); + Files.writeString(new File(projectHome, "p.hpl").toPath(), "

", StandardCharsets.UTF_8); + File zip = new File(tempDir, "dist.zip"); + SparkProjectPackage.exportProject( + projectHome.getAbsolutePath(), + zip.getAbsolutePath(), + new MemoryMetadataProvider(), + new Variables()); + + org.apache.spark.sql.SparkSession spark = + org.apache.spark.sql.SparkSession.builder() + .master("local[2]") + .appName("SparkProjectPackageTest") + .config("spark.ui.enabled", "false") + .config("spark.sql.shuffle.partitions", "2") + .getOrCreate(); + try { + Variables vars = new Variables(); + vars.setVariable(SparkProjectPackage.VAR_PACKAGE_URI, zip.getAbsolutePath()); + SparkProjectPackage.distributeToCluster(spark, vars); + assertEquals(zip.getName(), vars.getVariable(SparkProjectPackage.VAR_PACKAGE_SPARK_FILE)); + assertTrue(new File(vars.getVariable(SparkProjectPackage.VAR_PACKAGE_URI)).isFile()); + + String resolved = SparkProjectPackage.resolvePackagePathForMaterialize(vars); + assertNotNull(resolved); + assertTrue(new File(resolved).isFile()); + + SparkProjectPackage.ensureMaterializedOnWorker(vars); + assertTrue(new File(vars.getVariable("PROJECT_HOME"), "p.hpl").isFile()); + } finally { + spark.stop(); + } + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/run/MainSparkArgsTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/run/MainSparkArgsTest.java new file mode 100644 index 00000000000..11a6a08a635 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/run/MainSparkArgsTest.java @@ -0,0 +1,110 @@ +/* + * 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.hop.spark.run; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.hop.core.exception.HopException; +import org.junit.jupiter.api.Test; + +class MainSparkArgsTest { + + @Test + void parsePositionalThreeArgs() throws Exception { + MainSparkArgs args = + MainSparkArgs.parse(new String[] {"pipe.hpl", "meta.json", "spark-cluster"}); + assertEquals("pipe.hpl", args.getPipelinePath()); + assertEquals("meta.json", args.getMetadataPath()); + assertEquals("spark-cluster", args.getRunConfigName()); + assertNull(args.getEnvironmentFile()); + assertFalse(args.hasProjectPackage()); + } + + @Test + void parsePositionalWithEnvFile() throws Exception { + MainSparkArgs args = + MainSparkArgs.parse(new String[] {"pipe.hpl", "meta.json", "spark-cluster", "env.json"}); + assertEquals("env.json", args.getEnvironmentFile()); + } + + @Test + void parseNamedArgs() throws Exception { + MainSparkArgs args = + MainSparkArgs.parse( + new String[] { + "--HopPipelinePath=/data/p.hpl", + "--HopMetadataPath=/data/m.json", + "--HopRunConfigurationName=spark-native", + "--HopConfigFile=/data/env.json" + }); + assertEquals("/data/p.hpl", args.getPipelinePath()); + assertEquals("/data/m.json", args.getMetadataPath()); + assertEquals("spark-native", args.getRunConfigName()); + assertEquals("/data/env.json", args.getEnvironmentFile()); + } + + @Test + void parseProjectPackageModeWithoutMetadataPath() throws Exception { + MainSparkArgs args = + MainSparkArgs.parse( + new String[] { + "--HopProjectPackage=/tmp/pkg.zip", + "--HopPipelinePath=pipelines/run.hpl", + "--HopRunConfigurationName=spark-cluster" + }); + assertTrue(args.hasProjectPackage()); + assertEquals("/tmp/pkg.zip", args.getProjectPackage()); + assertEquals("pipelines/run.hpl", args.getPipelinePath()); + assertNull(args.getMetadataPath()); + assertEquals("spark-cluster", args.getRunConfigName()); + } + + @Test + void missingArgsFails() { + HopException e1 = assertThrows(HopException.class, () -> MainSparkArgs.parse(new String[] {})); + assertTrue(e1.getMessage().contains("No arguments")); + + HopException e2 = + assertThrows(HopException.class, () -> MainSparkArgs.parse(new String[] {"only.hpl"})); + assertTrue(e2.getMessage().contains("at least 3")); + + HopException e3 = + assertThrows( + HopException.class, + () -> + MainSparkArgs.parse( + new String[] {"--HopPipelinePath=a.hpl", "--HopMetadataPath=b.json" + // missing run config + })); + assertTrue(e3.getMessage().contains("required") || e3.getMessage().contains("Pipeline path")); + + HopException e4 = + assertThrows( + HopException.class, + () -> + MainSparkArgs.parse( + new String[] {"--HopPipelinePath=a.hpl", "--HopRunConfigurationName=spark" + // no package, no metadata + })); + assertTrue(e4.getMessage().contains("Metadata")); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/run/MainSparkDatabricksEnvTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/run/MainSparkDatabricksEnvTest.java new file mode 100644 index 00000000000..59e2c2bc21f --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/run/MainSparkDatabricksEnvTest.java @@ -0,0 +1,50 @@ +/* + * 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.hop.spark.run; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; + +class MainSparkDatabricksEnvTest { + + @Test + @DisabledIfEnvironmentVariable(named = "DATABRICKS_RUNTIME_VERSION", matches = ".+") + void isDatabricksEnvironmentFalseInNormalCi() { + // When neither DATABRICKS_RUNTIME_VERSION nor a /databricks SPARK_HOME is set (typical CI / + // laptop), detection must stay off so spark-submit still System.exit(0) on success. + String sparkHome = System.getenv("SPARK_HOME"); + if (sparkHome != null && sparkHome.contains("/databricks")) { + return; + } + assertFalse(MainSpark.isDatabricksEnvironment()); + } + + @Test + void isTrappedExitZeroMatchesDatabricksMessage() { + assertTrue( + MainSpark.isTrappedExitZero( + new SecurityException("Program attempted to exit with code 0"))); + assertFalse( + MainSpark.isTrappedExitZero( + new SecurityException("Program attempted to exit with code 1"))); + assertFalse(MainSpark.isTrappedExitZero(new RuntimeException("exit with code 0"))); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkCatalogApplierTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkCatalogApplierTest.java new file mode 100644 index 00000000000..b2ea45c754b --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkCatalogApplierTest.java @@ -0,0 +1,85 @@ +/* + * 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.hop.spark.table; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.spark.metadata.SparkCatalog; +import org.junit.jupiter.api.Test; + +class SparkCatalogApplierTest { + + @Test + void hadoopCatalogExpandsConf() throws Exception { + SparkCatalog cat = new SparkCatalog(); + cat.setName("my-lake"); + cat.setCatalogName("lake"); + cat.setCatalogType(SparkCatalog.TYPE_HADOOP); + cat.setWarehouse("/tmp/warehouse"); + cat.setConfExtra("io-impl=org.apache.iceberg.hadoop.HadoopFileIO\n# comment\n"); + + Map conf = SparkCatalogApplier.toSparkConfigs(cat, new Variables()); + assertEquals(SparkLakeFormats.ICEBERG_CATALOG, conf.get("spark.sql.catalog.lake")); + assertEquals("hadoop", conf.get("spark.sql.catalog.lake.type")); + assertTrue(conf.get("spark.sql.catalog.lake.warehouse").startsWith("file:")); + assertEquals( + "org.apache.iceberg.hadoop.HadoopFileIO", conf.get("spark.sql.catalog.lake.io-impl")); + } + + @Test + void restCatalogRequiresUri() { + SparkCatalog cat = new SparkCatalog(); + cat.setName("rest"); + cat.setCatalogName("remote"); + cat.setCatalogType(SparkCatalog.TYPE_REST); + assertThrows( + HopException.class, () -> SparkCatalogApplier.toSparkConfigs(cat, new Variables())); + } + + @Test + void restCatalogWithToken() throws Exception { + SparkCatalog cat = new SparkCatalog(); + cat.setName("rest"); + cat.setCatalogName("remote"); + cat.setCatalogType(SparkCatalog.TYPE_REST); + cat.setUri("https://catalog.example.com/iceberg"); + cat.setCredential("secret-token"); + Map conf = SparkCatalogApplier.toSparkConfigs(cat, new Variables()); + assertEquals("rest", conf.get("spark.sql.catalog.remote.type")); + assertEquals("https://catalog.example.com/iceberg", conf.get("spark.sql.catalog.remote.uri")); + assertEquals("secret-token", conf.get("spark.sql.catalog.remote.token")); + } + + @Test + void fullSparkKeyInConfExtra() throws Exception { + SparkCatalog cat = new SparkCatalog(); + cat.setName("c"); + cat.setCatalogName("c"); + cat.setCatalogType(SparkCatalog.TYPE_CUSTOM); + cat.setImplementation("com.example.MyCatalog"); + cat.setConfExtra("spark.sql.defaultCatalog=c"); + Map conf = SparkCatalogApplier.toSparkConfigs(cat, new Variables()); + assertEquals("com.example.MyCatalog", conf.get("spark.sql.catalog.c")); + assertEquals("c", conf.get("spark.sql.defaultCatalog")); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeConnectorProbeTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeConnectorProbeTest.java new file mode 100644 index 00000000000..4808f53c611 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeConnectorProbeTest.java @@ -0,0 +1,125 @@ +/* + * 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.hop.spark.table; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URL; +import java.net.URLClassLoader; +import java.util.List; +import java.util.Set; +import org.apache.hop.core.exception.HopException; +import org.junit.jupiter.api.Test; + +/** + * Default unit tests — no Delta/Iceberg JARs required. Uses an isolated empty classloader so probe + * failures are deterministic even if a developer has connectors on the main test classpath. + */ +class SparkLakeConnectorProbeTest { + + /** Classloader that cannot see application classes (no parent). */ + private static ClassLoader emptyClassLoader() { + return new URLClassLoader(new URL[0], /* parent */ null); + } + + @Test + void emptyFormatsDoesNotThrow() { + assertDoesNotThrow( + () -> SparkLakeConnectorProbe.verifyClasspath(List.of(), emptyClassLoader())); + assertDoesNotThrow(() -> SparkLakeConnectorProbe.verifyClasspath(null, emptyClassLoader())); + } + + @Test + void missingDeltaFailsWithActionableMessage() { + HopException ex = + assertThrows( + HopException.class, + () -> + SparkLakeConnectorProbe.verifyClasspath( + Set.of(SparkLakeFormats.FORMAT_DELTA), emptyClassLoader())); + String msg = ex.getMessage(); + assertTrue(msg.contains("Delta Lake"), msg); + assertTrue(msg.contains("plugins/engines/spark/lib"), msg); + assertTrue(msg.contains("--packages"), msg); + assertTrue(msg.contains("delta-spark_4.1_2.13"), msg); + assertTrue(msg.contains(SparkLakeFormats.DELTA_EXTENSION), msg); + } + + @Test + void missingIcebergFailsWithActionableMessage() { + HopException ex = + assertThrows( + HopException.class, + () -> + SparkLakeConnectorProbe.verifyClasspath( + Set.of(SparkLakeFormats.FORMAT_ICEBERG), emptyClassLoader())); + String msg = ex.getMessage(); + assertTrue(msg.contains("Iceberg"), msg); + assertTrue(msg.contains("plugins/engines/spark/lib"), msg); + assertTrue(msg.contains("--packages"), msg); + assertTrue(msg.contains("iceberg-spark-runtime-4.1_2.13"), msg); + assertTrue(msg.contains(SparkLakeFormats.ICEBERG_EXTENSIONS), msg); + } + + @Test + void unknownFormatFails() { + HopException ex = + assertThrows( + HopException.class, + () -> SparkLakeConnectorProbe.verifyClasspath(List.of("hudi"), emptyClassLoader())); + assertTrue(ex.getMessage().contains("Unsupported"), ex.getMessage()); + assertTrue(ex.getMessage().contains("hudi"), ex.getMessage()); + } + + @Test + void formatNamesAreNormalized() { + HopException ex = + assertThrows( + HopException.class, + () -> + SparkLakeConnectorProbe.verifyClasspath(List.of(" DELTA "), emptyClassLoader())); + assertTrue(ex.getMessage().contains("Delta Lake"), ex.getMessage()); + } + + @Test + void isClassPresentFalseOnEmptyLoader() { + assertFalse( + SparkLakeConnectorProbe.isClassPresent( + SparkLakeFormats.DELTA_EXTENSION, emptyClassLoader())); + } + + @Test + void isClassPresentTrueForJdkClass() { + assertTrue( + SparkLakeConnectorProbe.isClassPresent( + "java.lang.String", ClassLoader.getSystemClassLoader())); + } + + @Test + void missingMessageHelpersAreStable() { + String delta = SparkLakeConnectorProbe.missingDeltaMessage("x.Y"); + assertTrue(delta.contains("x.Y")); + assertTrue(delta.contains("4.3.1")); + String iceberg = SparkLakeConnectorProbe.missingIcebergMessage("a.B"); + assertTrue(iceberg.contains("a.B")); + assertTrue(iceberg.contains("1.11.0")); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeTableDeltaPathTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeTableDeltaPathTest.java new file mode 100644 index 00000000000..d0963c846f5 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeTableDeltaPathTest.java @@ -0,0 +1,179 @@ +/* + * 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.hop.spark.table; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.hop.core.HopEnvironment; +import org.apache.hop.core.logging.HopLogStore; +import org.apache.hop.core.logging.LogChannel; +import org.apache.hop.core.row.RowMeta; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.engines.SparkPipelineRunConfiguration; +import org.apache.hop.spark.pipeline.handler.SparkLakeTableInputHandler; +import org.apache.hop.spark.pipeline.handler.SparkLakeTableOutputHandler; +import org.apache.hop.spark.transforms.io.SparkFileOutputMeta; +import org.apache.hop.spark.transforms.table.SparkLakeTableInputMeta; +import org.apache.hop.spark.transforms.table.SparkLakeTableOutputMeta; +import org.apache.hop.spark.util.SparkConst; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** Delta PATH Input/Output handler round-trip (skipped if connectors missing). */ +class SparkLakeTableDeltaPathTest { + + @TempDir Path tempDir; + + private SparkSession spark; + + @BeforeAll + static void initHop() throws Exception { + HopEnvironment.init(); + HopLogStore.init(); + } + + @AfterEach + void stopSpark() { + if (spark != null) { + try { + spark.stop(); + } catch (Exception ignored) { + // best effort + } + spark = null; + } + try { + SparkSession.clearActiveSession(); + SparkSession.clearDefaultSession(); + } catch (Exception ignored) { + // ignore + } + } + + @Test + void deltaPathOutputThenInputRoundTrip() throws Exception { + assumeTrue( + SparkLakeConnectorProbe.isDeltaPresent(SparkLakeConnectorProbe.class.getClassLoader()), + "Delta connector not on classpath; connectors missing from test classpath"); + + Path tablePath = tempDir.resolve("orders_delta"); + spark = + SparkSession.builder() + .appName("hop-lake-delta-path") + .master("local[2]") + .config("spark.ui.enabled", "false") + .config("spark.ui.showConsoleProgress", "false") + .config("spark.metrics.staticSources.enabled", "false") + .config("spark.sql.shuffle.partitions", "2") + .config("spark.driver.host", "localhost") + .config(SparkLakeFormats.SPARK_CONF_EXTENSIONS, SparkLakeFormats.DELTA_EXTENSION) + .config(SparkLakeFormats.SPARK_CONF_SPARK_CATALOG, SparkLakeFormats.DELTA_CATALOG) + .getOrCreate(); + + Dataset source = spark.range(0, 20).toDF("id"); + + SparkLakeTableOutputMeta outMeta = new SparkLakeTableOutputMeta(); + outMeta.setFormat(SparkLakeFormats.FORMAT_DELTA); + outMeta.setIdentifierMode(SparkLakeTableInputMeta.MODE_PATH); + outMeta.setTablePath(tablePath.toString()); + // Overwrite so the test is idempotent + outMeta.setSaveMode(SparkFileOutputMeta.MODE_OVERWRITE); + + TransformMeta outTm = new TransformMeta("lake_out", outMeta); + outTm.setTransformPluginId(SparkConst.SPARK_LAKE_TABLE_OUTPUT_PLUGIN_ID); + PipelineMeta pipelineMeta = new PipelineMeta(); + pipelineMeta.addTransform(outTm); + + Map> map = new HashMap<>(); + new SparkLakeTableOutputHandler() + .handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + new MemoryMetadataProvider(), + "{}", + pipelineMeta, + outTm, + map, + spark, + new RowMeta(), + List.of(), + source); + + // Empty leaf after write + assertEquals(0, map.get("lake_out").count()); + + SparkLakeTableInputMeta inMeta = new SparkLakeTableInputMeta(); + inMeta.setFormat(SparkLakeFormats.FORMAT_DELTA); + inMeta.setIdentifierMode(SparkLakeTableInputMeta.MODE_PATH); + inMeta.setTablePath(tablePath.toString()); + + TransformMeta inTm = new TransformMeta("lake_in", inMeta); + inTm.setTransformPluginId(SparkConst.SPARK_LAKE_TABLE_INPUT_PLUGIN_ID); + pipelineMeta.addTransform(inTm); + + new SparkLakeTableInputHandler() + .handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + new MemoryMetadataProvider(), + "{}", + pipelineMeta, + inTm, + map, + spark, + new RowMeta(), + List.of(), + null); + + assertEquals(20L, map.get("lake_in").count()); + } + + @Test + void lakeSessionPlanCollectsDeltaFormat() throws Exception { + SparkLakeTableInputMeta inMeta = new SparkLakeTableInputMeta(); + inMeta.setFormat(SparkLakeFormats.FORMAT_DELTA); + inMeta.setTablePath("/tmp/x"); + TransformMeta inTm = new TransformMeta("in", inMeta); + inTm.setTransformPluginId(SparkConst.SPARK_LAKE_TABLE_INPUT_PLUGIN_ID); + + // Single-transform pipeline is active without hops + PipelineMeta pm = new PipelineMeta(); + pm.addTransform(inTm); + + LakeSessionPlan plan = LakeSessionPlan.from(pm, new MemoryMetadataProvider()); + assertEquals(1, plan.getFormatsNeeded().size()); + assertEquals(true, plan.needsDelta()); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeTableIcebergPathTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeTableIcebergPathTest.java new file mode 100644 index 00000000000..e9bf9b96dbb --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeTableIcebergPathTest.java @@ -0,0 +1,182 @@ +/* + * 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.hop.spark.table; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.hop.core.HopEnvironment; +import org.apache.hop.core.logging.HopLogStore; +import org.apache.hop.core.logging.LogChannel; +import org.apache.hop.core.row.RowMeta; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.engines.SparkPipelineRunConfiguration; +import org.apache.hop.spark.pipeline.handler.SparkLakeTableInputHandler; +import org.apache.hop.spark.pipeline.handler.SparkLakeTableOutputHandler; +import org.apache.hop.spark.transforms.io.SparkFileOutputMeta; +import org.apache.hop.spark.transforms.table.SparkLakeTableInputMeta; +import org.apache.hop.spark.transforms.table.SparkLakeTableOutputMeta; +import org.apache.hop.spark.util.SparkConst; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** Iceberg PATH Input/Output (skipped if connectors missing). */ +class SparkLakeTableIcebergPathTest { + + @TempDir Path tempDir; + + private SparkSession spark; + + @BeforeAll + static void initHop() throws Exception { + HopEnvironment.init(); + HopLogStore.init(); + } + + @AfterEach + void stopSpark() { + if (spark != null) { + try { + spark.stop(); + } catch (Exception ignored) { + // best effort + } + spark = null; + } + try { + SparkSession.clearActiveSession(); + SparkSession.clearDefaultSession(); + } catch (Exception ignored) { + // ignore + } + } + + @Test + void icebergPathOutputThenInputRoundTrip() throws Exception { + assumeTrue( + SparkLakeConnectorProbe.isIcebergPresent(SparkLakeConnectorProbe.class.getClassLoader()), + "Iceberg connector not on classpath; connectors missing from test classpath"); + + Path tablePath = tempDir.resolve("orders_iceberg"); + Path warehouse = tempDir.resolve("iceberg_wh"); + spark = + SparkSession.builder() + .appName("hop-lake-iceberg-path") + .master("local[2]") + .config("spark.ui.enabled", "false") + .config("spark.ui.showConsoleProgress", "false") + .config("spark.metrics.staticSources.enabled", "false") + .config("spark.sql.shuffle.partitions", "2") + .config("spark.driver.host", "localhost") + .config(SparkLakeFormats.SPARK_CONF_EXTENSIONS, SparkLakeFormats.ICEBERG_EXTENSIONS) + .config( + SparkLakeFormats.SPARK_CONF_ICEBERG_PATH_CATALOG, SparkLakeFormats.ICEBERG_CATALOG) + .config(SparkLakeFormats.SPARK_CONF_ICEBERG_PATH_CATALOG_TYPE, "hadoop") + .config( + SparkLakeFormats.SPARK_CONF_ICEBERG_PATH_CATALOG_WAREHOUSE, + warehouse.toUri().toString()) + .getOrCreate(); + + Dataset source = spark.range(0, 18).toDF("id"); + + SparkLakeTableOutputMeta outMeta = new SparkLakeTableOutputMeta(); + outMeta.setFormat(SparkLakeFormats.FORMAT_ICEBERG); + outMeta.setIdentifierMode(SparkLakeTableInputMeta.MODE_PATH); + outMeta.setTablePath(tablePath.toString()); + outMeta.setSaveMode(SparkFileOutputMeta.MODE_OVERWRITE); + + TransformMeta outTm = new TransformMeta("ice_out", outMeta); + outTm.setTransformPluginId(SparkConst.SPARK_LAKE_TABLE_OUTPUT_PLUGIN_ID); + PipelineMeta pipelineMeta = new PipelineMeta(); + pipelineMeta.addTransform(outTm); + + Map> map = new HashMap<>(); + new SparkLakeTableOutputHandler() + .handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + new MemoryMetadataProvider(), + "{}", + pipelineMeta, + outTm, + map, + spark, + new RowMeta(), + List.of(), + source); + + assertEquals(0, map.get("ice_out").count()); + + SparkLakeTableInputMeta inMeta = new SparkLakeTableInputMeta(); + inMeta.setFormat(SparkLakeFormats.FORMAT_ICEBERG); + inMeta.setIdentifierMode(SparkLakeTableInputMeta.MODE_PATH); + inMeta.setTablePath(tablePath.toString()); + + TransformMeta inTm = new TransformMeta("ice_in", inMeta); + inTm.setTransformPluginId(SparkConst.SPARK_LAKE_TABLE_INPUT_PLUGIN_ID); + pipelineMeta.addTransform(inTm); + + new SparkLakeTableInputHandler() + .handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + new MemoryMetadataProvider(), + "{}", + pipelineMeta, + inTm, + map, + spark, + new RowMeta(), + List.of(), + null); + + assertEquals(18L, map.get("ice_in").count()); + } + + @Test + void lakeSessionPlanCollectsIcebergFormat() throws Exception { + SparkLakeTableInputMeta inMeta = new SparkLakeTableInputMeta(); + inMeta.setFormat(SparkLakeFormats.FORMAT_ICEBERG); + inMeta.setTablePath("/tmp/x"); + TransformMeta inTm = new TransformMeta("in", inMeta); + inTm.setTransformPluginId(SparkConst.SPARK_LAKE_TABLE_INPUT_PLUGIN_ID); + + PipelineMeta pm = new PipelineMeta(); + pm.addTransform(inTm); + + LakeSessionPlan plan = LakeSessionPlan.from(pm, new MemoryMetadataProvider()); + assertEquals(true, plan.needsIceberg()); + assertEquals(false, plan.needsDelta()); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeTableMaintenanceTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeTableMaintenanceTest.java new file mode 100644 index 00000000000..05998f07aea --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeTableMaintenanceTest.java @@ -0,0 +1,206 @@ +/* + * 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.hop.spark.table; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.hop.core.HopEnvironment; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.HopLogStore; +import org.apache.hop.core.logging.LogChannel; +import org.apache.hop.core.row.RowMeta; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.engines.SparkPipelineRunConfiguration; +import org.apache.hop.spark.pipeline.handler.SparkLakeTableMaintenanceHandler; +import org.apache.hop.spark.pipeline.handler.SparkLakeTableOutputHandler; +import org.apache.hop.spark.transforms.io.SparkFileOutputMeta; +import org.apache.hop.spark.transforms.table.SparkLakeTableInputMeta; +import org.apache.hop.spark.transforms.table.SparkLakeTableMaintenanceMeta; +import org.apache.hop.spark.transforms.table.SparkLakeTableOutputMeta; +import org.apache.hop.spark.util.SparkConst; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** Delta OPTIMIZE smoke + destructive ack guard (OPTIMIZE skipped if connectors missing). */ +class SparkLakeTableMaintenanceTest { + + @TempDir Path tempDir; + + private SparkSession spark; + + @BeforeAll + static void initHop() throws Exception { + HopEnvironment.init(); + HopLogStore.init(); + } + + @AfterEach + void stopSpark() { + if (spark != null) { + try { + spark.stop(); + } catch (Exception ignored) { + // best effort + } + spark = null; + } + try { + SparkSession.clearActiveSession(); + SparkSession.clearDefaultSession(); + } catch (Exception ignored) { + // ignore + } + } + + @Test + void vacuumRequiresAcknowledge() { + SparkLakeTableMaintenanceMeta meta = new SparkLakeTableMaintenanceMeta(); + meta.setFormat(SparkLakeFormats.FORMAT_DELTA); + meta.setIdentifierMode(SparkLakeTableInputMeta.MODE_PATH); + meta.setTablePath("/tmp/t"); + meta.setOperation(SparkMaintenanceSqlBuilder.OP_VACUUM); + meta.setRetentionHours("168"); + meta.setAcknowledgeDestructive(false); + + TransformMeta tm = new TransformMeta("maint", meta); + tm.setTransformPluginId(SparkConst.SPARK_LAKE_TABLE_MAINTENANCE_PLUGIN_ID); + PipelineMeta pm = new PipelineMeta(); + pm.addTransform(tm); + + HopException ex = + assertThrows( + HopException.class, + () -> + new SparkLakeTableMaintenanceHandler() + .handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + new MemoryMetadataProvider(), + "{}", + pm, + tm, + new HashMap<>(), + null, + new RowMeta(), + List.of(), + null)); + assertTrue(ex.getMessage().contains("destructive") || ex.getMessage().contains("acknowledge")); + } + + @Test + void deltaOptimizeSmoke() throws Exception { + assumeTrue( + SparkLakeConnectorProbe.isDeltaPresent(SparkLakeConnectorProbe.class.getClassLoader()), + "Delta connector not on classpath; connectors missing from test classpath"); + + Path tablePath = tempDir.resolve("opt_table"); + spark = + SparkSession.builder() + .appName("hop-delta-opt") + .master("local[2]") + .config("spark.ui.enabled", "false") + .config("spark.driver.host", "localhost") + .config("spark.sql.shuffle.partitions", "2") + .config(SparkLakeFormats.SPARK_CONF_EXTENSIONS, SparkLakeFormats.DELTA_EXTENSION) + .config(SparkLakeFormats.SPARK_CONF_SPARK_CATALOG, SparkLakeFormats.DELTA_CATALOG) + .getOrCreate(); + + // Seed small table + writeDelta(tablePath.toString(), spark.range(0, 50).toDF("id")); + + SparkLakeTableMaintenanceMeta meta = new SparkLakeTableMaintenanceMeta(); + meta.setFormat(SparkLakeFormats.FORMAT_DELTA); + meta.setIdentifierMode(SparkLakeTableInputMeta.MODE_PATH); + meta.setTablePath(tablePath.toString()); + meta.setOperation(SparkMaintenanceSqlBuilder.OP_OPTIMIZE); + meta.setAcknowledgeDestructive(false); + + TransformMeta tm = new TransformMeta("opt", meta); + tm.setTransformPluginId(SparkConst.SPARK_LAKE_TABLE_MAINTENANCE_PLUGIN_ID); + PipelineMeta pm = new PipelineMeta(); + pm.addTransform(tm); + + Map> map = new HashMap<>(); + new SparkLakeTableMaintenanceHandler() + .handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + new MemoryMetadataProvider(), + "{}", + pm, + tm, + map, + spark, + new RowMeta(), + List.of(), + null); + + assertEquals(0, map.get("opt").count()); + assertEquals(50L, spark.read().format("delta").load(tablePath.toString()).count()); + } + + private static void assertTrue(boolean cond) { + org.junit.jupiter.api.Assertions.assertTrue(cond); + } + + private void writeDelta(String path, Dataset data) throws Exception { + SparkLakeTableOutputMeta outMeta = new SparkLakeTableOutputMeta(); + outMeta.setFormat(SparkLakeFormats.FORMAT_DELTA); + outMeta.setIdentifierMode(SparkLakeTableInputMeta.MODE_PATH); + outMeta.setTablePath(path); + outMeta.setSaveMode(SparkFileOutputMeta.MODE_OVERWRITE); + TransformMeta outTm = new TransformMeta("seed", outMeta); + outTm.setTransformPluginId(SparkConst.SPARK_LAKE_TABLE_OUTPUT_PLUGIN_ID); + PipelineMeta pm = new PipelineMeta(); + pm.addTransform(outTm); + Map> map = new HashMap<>(); + new SparkLakeTableOutputHandler() + .handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + new MemoryMetadataProvider(), + "{}", + pm, + outTm, + map, + spark, + new RowMeta(), + List.of(), + data); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeTableMergeTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeTableMergeTest.java new file mode 100644 index 00000000000..09f89997c90 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeTableMergeTest.java @@ -0,0 +1,203 @@ +/* + * 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.hop.spark.table; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.hop.core.HopEnvironment; +import org.apache.hop.core.logging.HopLogStore; +import org.apache.hop.core.logging.LogChannel; +import org.apache.hop.core.row.RowMeta; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.engines.SparkPipelineRunConfiguration; +import org.apache.hop.spark.pipeline.handler.SparkLakeTableMergeHandler; +import org.apache.hop.spark.pipeline.handler.SparkLakeTableOutputHandler; +import org.apache.hop.spark.transforms.io.SparkFileOutputMeta; +import org.apache.hop.spark.transforms.table.SparkLakeTableInputMeta; +import org.apache.hop.spark.transforms.table.SparkLakeTableMergeMeta; +import org.apache.hop.spark.transforms.table.SparkLakeTableOutputMeta; +import org.apache.hop.spark.util.SparkConst; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.RowFactory; +import org.apache.spark.sql.SparkSession; +import org.apache.spark.sql.types.DataTypes; +import org.apache.spark.sql.types.StructField; +import org.apache.spark.sql.types.StructType; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** Delta PATH MERGE upsert (skipped if connectors missing). */ +class SparkLakeTableMergeTest { + + @TempDir Path tempDir; + + private SparkSession spark; + + @BeforeAll + static void initHop() throws Exception { + HopEnvironment.init(); + HopLogStore.init(); + } + + @AfterEach + void stopSpark() { + if (spark != null) { + try { + spark.stop(); + } catch (Exception ignored) { + // best effort + } + spark = null; + } + try { + SparkSession.clearActiveSession(); + SparkSession.clearDefaultSession(); + } catch (Exception ignored) { + // ignore + } + } + + @Test + void deltaPathMergeUpsert() throws Exception { + assumeTrue( + SparkLakeConnectorProbe.isDeltaPresent(SparkLakeConnectorProbe.class.getClassLoader()), + "Delta connector not on classpath; connectors missing from test classpath"); + + Path tablePath = tempDir.resolve("merge_orders"); + spark = + SparkSession.builder() + .appName("hop-delta-merge") + .master("local[2]") + .config("spark.ui.enabled", "false") + .config("spark.driver.host", "localhost") + .config("spark.sql.shuffle.partitions", "2") + .config(SparkLakeFormats.SPARK_CONF_EXTENSIONS, SparkLakeFormats.DELTA_EXTENSION) + .config(SparkLakeFormats.SPARK_CONF_SPARK_CATALOG, SparkLakeFormats.DELTA_CATALOG) + .getOrCreate(); + + StructType schema = + new StructType( + new StructField[] { + DataTypes.createStructField("id", DataTypes.LongType, false), + DataTypes.createStructField("val", DataTypes.StringType, true) + }); + + // Seed target: id 1,2 + Dataset seed = + spark.createDataFrame( + List.of(RowFactory.create(1L, "a"), RowFactory.create(2L, "b")), schema); + writeDelta(tablePath.toString(), seed); + + // Source: update id=1, insert id=3 + Dataset source = + spark.createDataFrame( + List.of(RowFactory.create(1L, "a-updated"), RowFactory.create(3L, "c")), schema); + + SparkLakeTableMergeMeta mergeMeta = new SparkLakeTableMergeMeta(); + mergeMeta.setFormat(SparkLakeFormats.FORMAT_DELTA); + mergeMeta.setIdentifierMode(SparkLakeTableInputMeta.MODE_PATH); + mergeMeta.setTablePath(tablePath.toString()); + mergeMeta.setMergeCondition("t.id = s.id"); + mergeMeta.setMatchedAction(SparkMergeSqlBuilder.MATCHED_UPDATE_ALL); + mergeMeta.setNotMatchedAction(SparkMergeSqlBuilder.NOT_MATCHED_INSERT_ALL); + + TransformMeta mergeTm = new TransformMeta("merge", mergeMeta); + mergeTm.setTransformPluginId(SparkConst.SPARK_LAKE_TABLE_MERGE_PLUGIN_ID); + PipelineMeta pm = new PipelineMeta(); + pm.addTransform(mergeTm); + + Map> map = new HashMap<>(); + new SparkLakeTableMergeHandler() + .handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + new MemoryMetadataProvider(), + "{}", + pm, + mergeTm, + map, + spark, + new RowMeta(), + List.of(), + source); + + assertEquals(0, map.get("merge").count()); + + Dataset result = + spark.read().format(SparkLakeFormats.FORMAT_DELTA).load(tablePath.toString()).orderBy("id"); + assertEquals(3L, result.count()); + List rows = result.collectAsList(); + assertEquals(1L, rows.get(0).getLong(0)); + assertEquals("a-updated", rows.get(0).getString(1)); + assertEquals(2L, rows.get(1).getLong(0)); + assertEquals("b", rows.get(1).getString(1)); + assertEquals(3L, rows.get(2).getLong(0)); + assertEquals("c", rows.get(2).getString(1)); + } + + @Test + void resolveMergeTargetDeltaPath() throws Exception { + SparkLakeTableMergeMeta meta = new SparkLakeTableMergeMeta(); + meta.setFormat(SparkLakeFormats.FORMAT_DELTA); + meta.setIdentifierMode(SparkLakeTableInputMeta.MODE_PATH); + meta.setTablePath("/tmp/orders"); + String id = SparkLakeTableSupport.resolveMergeTargetSqlId(null, new Variables(), meta, "m"); + assertEquals("delta.`/tmp/orders`", id); + } + + private void writeDelta(String path, Dataset data) throws Exception { + SparkLakeTableOutputMeta outMeta = new SparkLakeTableOutputMeta(); + outMeta.setFormat(SparkLakeFormats.FORMAT_DELTA); + outMeta.setIdentifierMode(SparkLakeTableInputMeta.MODE_PATH); + outMeta.setTablePath(path); + outMeta.setSaveMode(SparkFileOutputMeta.MODE_OVERWRITE); + TransformMeta outTm = new TransformMeta("seed", outMeta); + outTm.setTransformPluginId(SparkConst.SPARK_LAKE_TABLE_OUTPUT_PLUGIN_ID); + PipelineMeta pm = new PipelineMeta(); + pm.addTransform(outTm); + Map> map = new HashMap<>(); + new SparkLakeTableOutputHandler() + .handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + new MemoryMetadataProvider(), + "{}", + pm, + outTm, + map, + spark, + new RowMeta(), + List.of(), + data); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeTableSupportTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeTableSupportTest.java new file mode 100644 index 00000000000..59889026b79 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeTableSupportTest.java @@ -0,0 +1,202 @@ +/* + * 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.hop.spark.table; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.spark.transforms.table.SparkLakeTableInputMeta; +import org.apache.hop.spark.transforms.table.SparkLakeTableOutputMeta; +import org.junit.jupiter.api.Test; + +/** Unit tests that do not require Delta/Iceberg connectors on the classpath. */ +class SparkLakeTableSupportTest { + + @Test + void normalizeFormatDefaultsAndValidates() throws Exception { + assertEquals(SparkLakeFormats.FORMAT_DELTA, SparkLakeTableSupport.normalizeFormat(null)); + assertEquals(SparkLakeFormats.FORMAT_DELTA, SparkLakeTableSupport.normalizeFormat(" DELTA ")); + assertEquals(SparkLakeFormats.FORMAT_ICEBERG, SparkLakeTableSupport.normalizeFormat("iceberg")); + HopException ex = + assertThrows(HopException.class, () -> SparkLakeTableSupport.normalizeFormat("hudi")); + assertTrue(ex.getMessage().contains("Unsupported")); + } + + @Test + void normalizeIdentifierMode() throws Exception { + assertEquals( + SparkLakeTableInputMeta.MODE_PATH, SparkLakeTableSupport.normalizeIdentifierMode(null)); + assertEquals( + SparkLakeTableInputMeta.MODE_TABLE, SparkLakeTableSupport.normalizeIdentifierMode("table")); + assertThrows(HopException.class, () -> SparkLakeTableSupport.normalizeIdentifierMode("uri")); + } + + @Test + void collectFormatAddsNormalized() throws Exception { + Set set = new LinkedHashSet<>(); + SparkLakeTableSupport.collectFormat("Delta", set); + assertTrue(set.contains(SparkLakeFormats.FORMAT_DELTA)); + } + + @Test + void icebergPathSqlIdentifierQuotesUri() { + String id = SparkLakeTableSupport.icebergPathSqlIdentifier("/tmp/orders"); + assertTrue(id.startsWith(SparkLakeFormats.ICEBERG_PATH_CATALOG_NAME + ".`")); + assertTrue(id.endsWith("`")); + assertTrue(id.contains("file:")); + assertTrue(id.contains("orders")); + } + + @Test + void toTableLocationUriPreservesSchemes() { + assertEquals("s3a://bucket/t", SparkLakeTableSupport.toTableLocationUri("s3a://bucket/t")); + assertTrue(SparkLakeTableSupport.toTableLocationUri("/tmp/x").startsWith("file:")); + } + + @Test + void resolveLakeTargetAppliesPathSchemeMap() throws Exception { + String id = + SparkLakeTableSupport.resolveLakeTargetSqlId( + null, + new Variables(), + SparkLakeFormats.FORMAT_DELTA, + SparkLakeTableInputMeta.MODE_PATH, + "s3://bucket/table", + null, + "merge", + "Merge", + "s3=s3a"); + assertEquals("delta.`s3a://bucket/table`", id); + } + + @Test + void resolveTableIdentifierPrefixesCatalog() throws Exception { + assertEquals( + "lake.db.orders", + SparkLakeTableSupport.resolveTableIdentifier("db.orders", "lake", new Variables())); + assertEquals( + "lake.db.orders", + SparkLakeTableSupport.resolveTableIdentifier("lake.db.orders", "lake", new Variables())); + } + + @Test + void resolveTableIdentifierRequiresValue() { + assertThrows( + HopException.class, + () -> SparkLakeTableSupport.resolveTableIdentifier("", "lake", new Variables())); + } + + @Test + void resolveWriteRejectsMissingPath() { + SparkLakeTableOutputMeta meta = new SparkLakeTableOutputMeta(); + meta.setFormat(SparkLakeFormats.FORMAT_DELTA); + meta.setIdentifierMode(SparkLakeTableInputMeta.MODE_PATH); + meta.setTablePath(""); + HopException ex = + assertThrows( + HopException.class, + () -> SparkLakeTableSupport.resolveWrite(null, null, new Variables(), "out", meta)); + assertTrue(ex.getMessage().contains("path")); + } + + @Test + void defaultSaveModeIsErrorIfExists() { + SparkLakeTableOutputMeta meta = new SparkLakeTableOutputMeta(); + assertEquals( + org.apache.hop.spark.transforms.io.SparkFileOutputMeta.MODE_ERROR, meta.getSaveMode()); + } + + @Test + void timeTravelOptionMapDelta() throws Exception { + Map v = + SparkLakeTableSupport.timeTravelOptionMap( + SparkLakeFormats.FORMAT_DELTA, SparkLakeTableInputMeta.TIME_TRAVEL_VERSION, "12", null); + assertEquals("12", v.get("versionAsOf")); + assertEquals(1, v.size()); + + Map t = + SparkLakeTableSupport.timeTravelOptionMap( + SparkLakeFormats.FORMAT_DELTA, + SparkLakeTableInputMeta.TIME_TRAVEL_TIMESTAMP, + null, + "2024-01-15 10:00:00"); + assertEquals("2024-01-15 10:00:00", t.get("timestampAsOf")); + } + + @Test + void timeTravelOptionMapIceberg() throws Exception { + Map v = + SparkLakeTableSupport.timeTravelOptionMap( + SparkLakeFormats.FORMAT_ICEBERG, + SparkLakeTableInputMeta.TIME_TRAVEL_VERSION, + "999", + null); + assertEquals("999", v.get("snapshot-id")); + + Map t = + SparkLakeTableSupport.timeTravelOptionMap( + SparkLakeFormats.FORMAT_ICEBERG, + SparkLakeTableInputMeta.TIME_TRAVEL_TIMESTAMP, + null, + "2024-06-01 12:00:00"); + assertEquals("2024-06-01 12:00:00", t.get("as-of-timestamp")); + } + + @Test + void timeTravelNoneYieldsEmptyMap() throws Exception { + assertTrue( + SparkLakeTableSupport.timeTravelOptionMap( + SparkLakeFormats.FORMAT_DELTA, SparkLakeTableInputMeta.TIME_TRAVEL_NONE, "1", "t") + .isEmpty()); + } + + @Test + void timeTravelVersionRequiresValue() { + assertThrows( + HopException.class, + () -> + SparkLakeTableSupport.timeTravelOptionMap( + SparkLakeFormats.FORMAT_DELTA, + SparkLakeTableInputMeta.TIME_TRAVEL_VERSION, + "", + null)); + } + + @Test + void icebergTimeTravelSql() throws Exception { + String id = "hop_iceberg.`file:///tmp/t`"; + assertEquals( + "SELECT * FROM " + id, + SparkLakeTableSupport.buildIcebergTimeTravelSql( + id, SparkLakeTableInputMeta.TIME_TRAVEL_NONE, null, null)); + assertEquals( + "SELECT * FROM " + id + " VERSION AS OF 42", + SparkLakeTableSupport.buildIcebergTimeTravelSql( + id, SparkLakeTableInputMeta.TIME_TRAVEL_VERSION, "42", null)); + assertEquals( + "SELECT * FROM " + id + " TIMESTAMP AS OF TIMESTAMP '2024-01-01 00:00:00'", + SparkLakeTableSupport.buildIcebergTimeTravelSql( + id, SparkLakeTableInputMeta.TIME_TRAVEL_TIMESTAMP, null, "2024-01-01 00:00:00")); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeTableTableModeTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeTableTableModeTest.java new file mode 100644 index 00000000000..d1d19acec55 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeTableTableModeTest.java @@ -0,0 +1,202 @@ +/* + * 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.hop.spark.table; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.hop.core.HopEnvironment; +import org.apache.hop.core.logging.HopLogStore; +import org.apache.hop.core.logging.LogChannel; +import org.apache.hop.core.row.RowMeta; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.engines.SparkPipelineRunConfiguration; +import org.apache.hop.spark.metadata.SparkCatalog; +import org.apache.hop.spark.pipeline.handler.SparkLakeTableInputHandler; +import org.apache.hop.spark.pipeline.handler.SparkLakeTableOutputHandler; +import org.apache.hop.spark.transforms.io.SparkFileOutputMeta; +import org.apache.hop.spark.transforms.table.SparkLakeTableInputMeta; +import org.apache.hop.spark.transforms.table.SparkLakeTableOutputMeta; +import org.apache.hop.spark.util.SparkConst; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** Iceberg TABLE mode via SparkCatalog (Hadoop warehouse; skipped if connectors missing). */ +class SparkLakeTableTableModeTest { + + @TempDir Path tempDir; + + private SparkSession spark; + + @BeforeAll + static void initHop() throws Exception { + HopEnvironment.init(); + HopLogStore.init(); + } + + @AfterEach + void stopSpark() { + if (spark != null) { + try { + spark.stop(); + } catch (Exception ignored) { + // best effort + } + spark = null; + } + try { + SparkSession.clearActiveSession(); + SparkSession.clearDefaultSession(); + } catch (Exception ignored) { + // ignore + } + } + + @Test + void icebergTableModeRoundTripWithSparkCatalog() throws Exception { + assumeTrue( + SparkLakeConnectorProbe.isIcebergPresent(SparkLakeConnectorProbe.class.getClassLoader()), + "Iceberg connector not on classpath; connectors missing from test classpath"); + + Path warehouse = tempDir.resolve("wh"); + SparkCatalog catalogMeta = new SparkCatalog(); + catalogMeta.setName("lake-meta"); + catalogMeta.setCatalogName("lake"); + catalogMeta.setCatalogType(SparkCatalog.TYPE_HADOOP); + catalogMeta.setWarehouse(warehouse.toUri().toString()); + + MemoryMetadataProvider provider = new MemoryMetadataProvider(); + provider.getSerializer(SparkCatalog.class).save(catalogMeta); + + // Build session as LakeSessionPlan would for hop-run + SparkSession.Builder builder = + SparkSession.builder() + .appName("hop-iceberg-table") + .master("local[2]") + .config("spark.ui.enabled", "false") + .config("spark.driver.host", "localhost") + .config("spark.sql.shuffle.partitions", "2") + .config(SparkLakeFormats.SPARK_CONF_EXTENSIONS, SparkLakeFormats.ICEBERG_EXTENSIONS); + SparkCatalogApplier.applyToBuilder(builder, catalogMeta, new Variables()); + spark = builder.getOrCreate(); + + String tableId = "lake.db.orders"; + + SparkLakeTableOutputMeta outMeta = new SparkLakeTableOutputMeta(); + outMeta.setFormat(SparkLakeFormats.FORMAT_ICEBERG); + outMeta.setIdentifierMode(SparkLakeTableInputMeta.MODE_TABLE); + outMeta.setTableIdentifier(tableId); + outMeta.setCatalogMetadataName("lake-meta"); + outMeta.setSaveMode(SparkFileOutputMeta.MODE_OVERWRITE); + + TransformMeta outTm = new TransformMeta("out", outMeta); + outTm.setTransformPluginId(SparkConst.SPARK_LAKE_TABLE_OUTPUT_PLUGIN_ID); + PipelineMeta pm = new PipelineMeta(); + pm.addTransform(outTm); + + Map> map = new HashMap<>(); + new SparkLakeTableOutputHandler() + .handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + provider, + "{}", + pm, + outTm, + map, + spark, + new RowMeta(), + List.of(), + spark.range(0, 12).toDF("id")); + + assertEquals(0, map.get("out").count()); + assertEquals(12L, spark.table(tableId).count()); + + SparkLakeTableInputMeta inMeta = new SparkLakeTableInputMeta(); + inMeta.setFormat(SparkLakeFormats.FORMAT_ICEBERG); + inMeta.setIdentifierMode(SparkLakeTableInputMeta.MODE_TABLE); + inMeta.setTableIdentifier(tableId); + inMeta.setCatalogMetadataName("lake-meta"); + + TransformMeta inTm = new TransformMeta("in", inMeta); + inTm.setTransformPluginId(SparkConst.SPARK_LAKE_TABLE_INPUT_PLUGIN_ID); + pm.addTransform(inTm); + + new SparkLakeTableInputHandler() + .handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + provider, + "{}", + pm, + inTm, + map, + spark, + new RowMeta(), + List.of(), + null); + + assertEquals(12L, map.get("in").count()); + } + + @Test + void lakeSessionPlanLoadsSparkCatalog() throws Exception { + SparkCatalog catalogMeta = new SparkCatalog(); + catalogMeta.setName("lake-meta"); + catalogMeta.setCatalogName("lake"); + catalogMeta.setCatalogType(SparkCatalog.TYPE_HADOOP); + catalogMeta.setWarehouse("/tmp/wh"); + + MemoryMetadataProvider provider = new MemoryMetadataProvider(); + provider.getSerializer(SparkCatalog.class).save(catalogMeta); + + SparkLakeTableInputMeta inMeta = new SparkLakeTableInputMeta(); + inMeta.setFormat(SparkLakeFormats.FORMAT_ICEBERG); + inMeta.setIdentifierMode(SparkLakeTableInputMeta.MODE_TABLE); + inMeta.setTableIdentifier("lake.db.t"); + inMeta.setCatalogMetadataName("lake-meta"); + + TransformMeta inTm = new TransformMeta("in", inMeta); + inTm.setTransformPluginId(SparkConst.SPARK_LAKE_TABLE_INPUT_PLUGIN_ID); + PipelineMeta pm = new PipelineMeta(); + pm.addTransform(inTm); + + LakeSessionPlan plan = LakeSessionPlan.from(pm, provider, new Variables()); + assertTrue(plan.needsIceberg()); + assertTrue(plan.needsTableMode()); + assertEquals(1, plan.getCatalogsByMetaName().size()); + assertTrue(plan.getCatalogsByMetaName().containsKey("lake-meta")); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeTableTimeTravelTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeTableTimeTravelTest.java new file mode 100644 index 00000000000..9b9182b8541 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakeTableTimeTravelTest.java @@ -0,0 +1,222 @@ +/* + * 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.hop.spark.table; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.hop.core.HopEnvironment; +import org.apache.hop.core.logging.HopLogStore; +import org.apache.hop.core.logging.LogChannel; +import org.apache.hop.core.row.RowMeta; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.engines.SparkPipelineRunConfiguration; +import org.apache.hop.spark.pipeline.handler.SparkLakeTableInputHandler; +import org.apache.hop.spark.pipeline.handler.SparkLakeTableOutputHandler; +import org.apache.hop.spark.transforms.io.SparkFileOutputMeta; +import org.apache.hop.spark.transforms.table.SparkLakeTableInputMeta; +import org.apache.hop.spark.transforms.table.SparkLakeTableOutputMeta; +import org.apache.hop.spark.util.SparkConst; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** Time travel: write twice, read as-of first version/snapshot (skipped if connectors missing). */ +class SparkLakeTableTimeTravelTest { + + @TempDir Path tempDir; + + private SparkSession spark; + + @BeforeAll + static void initHop() throws Exception { + HopEnvironment.init(); + HopLogStore.init(); + } + + @AfterEach + void stopSpark() { + if (spark != null) { + try { + spark.stop(); + } catch (Exception ignored) { + // best effort + } + spark = null; + } + try { + SparkSession.clearActiveSession(); + SparkSession.clearDefaultSession(); + } catch (Exception ignored) { + // ignore + } + } + + @Test + void deltaVersionAsOfFirstWrite() throws Exception { + assumeTrue( + SparkLakeConnectorProbe.isDeltaPresent(SparkLakeConnectorProbe.class.getClassLoader()), + "Delta connector not on classpath; connectors missing from test classpath"); + + Path tablePath = tempDir.resolve("delta_tt"); + spark = + SparkSession.builder() + .appName("hop-delta-tt") + .master("local[2]") + .config("spark.ui.enabled", "false") + .config("spark.driver.host", "localhost") + .config("spark.sql.shuffle.partitions", "2") + .config(SparkLakeFormats.SPARK_CONF_EXTENSIONS, SparkLakeFormats.DELTA_EXTENSION) + .config(SparkLakeFormats.SPARK_CONF_SPARK_CATALOG, SparkLakeFormats.DELTA_CATALOG) + .getOrCreate(); + + writeLake(tablePath.toString(), SparkLakeFormats.FORMAT_DELTA, spark.range(0, 5).toDF("id")); + writeLake(tablePath.toString(), SparkLakeFormats.FORMAT_DELTA, spark.range(0, 20).toDF("id")); + + // Current = 20 + assertEquals(20L, readLake(tablePath.toString(), SparkLakeFormats.FORMAT_DELTA, null, null)); + + // version 0 is first write (5 rows) on a new path table after overwrite creates version 0,1 + assertEquals( + 5L, + readLake( + tablePath.toString(), + SparkLakeFormats.FORMAT_DELTA, + SparkLakeTableInputMeta.TIME_TRAVEL_VERSION, + "0")); + } + + @Test + void icebergSnapshotAsOfFirstWrite() throws Exception { + assumeTrue( + SparkLakeConnectorProbe.isIcebergPresent(SparkLakeConnectorProbe.class.getClassLoader()), + "Iceberg connector not on classpath; connectors missing from test classpath"); + + Path tablePath = tempDir.resolve("iceberg_tt"); + Path warehouse = tempDir.resolve("wh"); + spark = + SparkSession.builder() + .appName("hop-iceberg-tt") + .master("local[2]") + .config("spark.ui.enabled", "false") + .config("spark.driver.host", "localhost") + .config("spark.sql.shuffle.partitions", "2") + .config(SparkLakeFormats.SPARK_CONF_EXTENSIONS, SparkLakeFormats.ICEBERG_EXTENSIONS) + .config( + SparkLakeFormats.SPARK_CONF_ICEBERG_PATH_CATALOG, SparkLakeFormats.ICEBERG_CATALOG) + .config(SparkLakeFormats.SPARK_CONF_ICEBERG_PATH_CATALOG_TYPE, "hadoop") + .config( + SparkLakeFormats.SPARK_CONF_ICEBERG_PATH_CATALOG_WAREHOUSE, + warehouse.toUri().toString()) + .getOrCreate(); + + writeLake(tablePath.toString(), SparkLakeFormats.FORMAT_ICEBERG, spark.range(0, 7).toDF("id")); + writeLake(tablePath.toString(), SparkLakeFormats.FORMAT_ICEBERG, spark.range(0, 15).toDF("id")); + + assertEquals(15L, readLake(tablePath.toString(), SparkLakeFormats.FORMAT_ICEBERG, null, null)); + + String sqlId = SparkLakeTableSupport.icebergPathSqlIdentifier(tablePath.toString()); + List snaps = spark.sql("SELECT snapshot_id FROM " + sqlId + ".snapshots").collectAsList(); + // First committed snapshot corresponds to first write + long firstSnap = snaps.get(0).getLong(0); + + assertEquals( + 7L, + readLake( + tablePath.toString(), + SparkLakeFormats.FORMAT_ICEBERG, + SparkLakeTableInputMeta.TIME_TRAVEL_VERSION, + Long.toString(firstSnap))); + } + + private void writeLake(String path, String format, Dataset data) throws Exception { + SparkLakeTableOutputMeta outMeta = new SparkLakeTableOutputMeta(); + outMeta.setFormat(format); + outMeta.setIdentifierMode(SparkLakeTableInputMeta.MODE_PATH); + outMeta.setTablePath(path); + outMeta.setSaveMode(SparkFileOutputMeta.MODE_OVERWRITE); + + TransformMeta outTm = new TransformMeta("out", outMeta); + outTm.setTransformPluginId(SparkConst.SPARK_LAKE_TABLE_OUTPUT_PLUGIN_ID); + PipelineMeta pm = new PipelineMeta(); + pm.addTransform(outTm); + + Map> map = new HashMap<>(); + new SparkLakeTableOutputHandler() + .handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + new MemoryMetadataProvider(), + "{}", + pm, + outTm, + map, + spark, + new RowMeta(), + List.of(), + data); + } + + private long readLake(String path, String format, String ttType, String version) + throws Exception { + SparkLakeTableInputMeta inMeta = new SparkLakeTableInputMeta(); + inMeta.setFormat(format); + inMeta.setIdentifierMode(SparkLakeTableInputMeta.MODE_PATH); + inMeta.setTablePath(path); + if (ttType != null) { + inMeta.setTimeTravelType(ttType); + inMeta.setTimeTravelVersion(version); + } + + TransformMeta inTm = new TransformMeta("in", inMeta); + inTm.setTransformPluginId(SparkConst.SPARK_LAKE_TABLE_INPUT_PLUGIN_ID); + PipelineMeta pm = new PipelineMeta(); + pm.addTransform(inTm); + + Map> map = new HashMap<>(); + new SparkLakeTableInputHandler() + .handleTransform( + LogChannel.GENERAL, + new Variables(), + "spark", + new SparkPipelineRunConfiguration(), + new MemoryMetadataProvider(), + "{}", + pm, + inTm, + map, + spark, + new RowMeta(), + List.of(), + null); + return map.get("in").count(); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakehouseConnectorTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakehouseConnectorTest.java new file mode 100644 index 00000000000..255592ea87b --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkLakehouseConnectorTest.java @@ -0,0 +1,266 @@ +/* + * 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.hop.spark.table; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import org.apache.spark.sql.Dataset; +import org.apache.spark.sql.Row; +import org.apache.spark.sql.SparkSession; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.io.TempDir; + +/** + * Lakehouse packaging / classpath spike. Connectors are default runtime dependencies of {@code + * hop-engines-spark}; assumptions skip only if the classpath is incomplete. + * + *

Success criteria: + * + *

    + *
  1. {@code Class.forName(DeltaSparkSessionExtension)} under the test / engine CL + *
  2. Delta PATH write/read with extension + {@code DeltaCatalog} + *
  3. Record whether PATH I/O works without {@code DeltaCatalog} (feeds tiered probe defaults) + *
  4. Iceberg classpath + Hadoop-warehouse / path smoke when the Iceberg runtime is present + *
+ */ +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class SparkLakehouseConnectorTest { + + @TempDir Path tempDir; + + private SparkSession spark; + + @BeforeAll + static void requireLakehouseProfileHint() { + // Soft documentation for developers when the suite is skipped. + if (!SparkLakeConnectorProbe.isDeltaPresent(SparkLakeConnectorProbe.class.getClassLoader())) { + System.err.println( + "SparkLakehouseConnectorTest: Delta not on classpath — " + + "rebuild hop-engines-spark with default runtime deps."); + } + } + + @AfterEach + void stopSpark() { + if (spark != null) { + try { + spark.stop(); + } catch (Exception ignored) { + // best effort + } + spark = null; + } + // Clear active/default session so the next test can rebuild with different conf. + try { + SparkSession.clearActiveSession(); + SparkSession.clearDefaultSession(); + } catch (Exception ignored) { + // Spark API availability may vary; ignore + } + } + + @Test + @Order(1) + void deltaExtensionIsLoadableWhenConnectorPresent() { + assumeDelta(); + ClassLoader cl = SparkLakeConnectorProbe.class.getClassLoader(); + assertTrue(SparkLakeConnectorProbe.isClassPresent(SparkLakeFormats.DELTA_EXTENSION, cl)); + assertTrue(SparkLakeConnectorProbe.isClassPresent(SparkLakeFormats.DELTA_CATALOG, cl)); + assertDoesNotThrow( + () -> SparkLakeConnectorProbe.verifyClasspath(Set.of(SparkLakeFormats.FORMAT_DELTA), cl)); + } + + /** + * Primary spike gate: PATH round-trip with modern Delta 4.x session conf (extension + + * DeltaCatalog). + */ + @Test + @Order(2) + void deltaPathRoundTripWithExtensionAndDeltaCatalog() { + assumeDelta(); + Path tablePath = tempDir.resolve("delta_with_catalog"); + spark = + newSessionBuilder("hop-delta-with-catalog") + .config(SparkLakeFormats.SPARK_CONF_EXTENSIONS, SparkLakeFormats.DELTA_EXTENSION) + .config(SparkLakeFormats.SPARK_CONF_SPARK_CATALOG, SparkLakeFormats.DELTA_CATALOG) + .getOrCreate(); + + Dataset written = spark.range(0, 25).toDF("id"); + written + .write() + .format(SparkLakeFormats.FORMAT_DELTA) + .mode("overwrite") + .save(tablePath.toString()); + + long count = + spark.read().format(SparkLakeFormats.FORMAT_DELTA).load(tablePath.toString()).count(); + assertEquals(25L, count); + } + + /** + * Records whether pure PATH {@code format("delta")} works without registering DeltaCatalog. + * + *

Spike finding (Delta 4.3.1 / Spark 4.1.2): PATH write/read requires + * DeltaCatalog — Delta fails with {@code + * DELTA_CONFIGURE_SPARK_SESSION_WITH_EXTENSION_AND_CATALOG} if only the extension is set. Tiered + * probe (KD-15) must hard-fail missing DeltaCatalog for any Delta use, including PATH-only. + */ + @Test + @Order(3) + void deltaPathIoWithoutDeltaCatalog_recordsBehavior() { + assumeDelta(); + Path tablePath = tempDir.resolve("delta_no_catalog"); + + // Session with extension only — no spark.sql.catalog.spark_catalog=DeltaCatalog + spark = + newSessionBuilder("hop-delta-no-catalog") + .config(SparkLakeFormats.SPARK_CONF_EXTENSIONS, SparkLakeFormats.DELTA_EXTENSION) + .getOrCreate(); + + boolean pathIoSucceeded; + String detail; + try { + spark + .range(0, 10) + .toDF("id") + .write() + .format(SparkLakeFormats.FORMAT_DELTA) + .mode("overwrite") + .save(tablePath.toString()); + long count = + spark.read().format(SparkLakeFormats.FORMAT_DELTA).load(tablePath.toString()).count(); + pathIoSucceeded = count == 10L; + detail = "PATH write/read succeeded without DeltaCatalog (count=" + count + ")"; + } catch (Exception e) { + pathIoSucceeded = false; + detail = + "PATH write/read failed without DeltaCatalog: " + + e.getClass().getName() + + ": " + + e.getMessage(); + } + + System.out.println("SPIKE_FINDING deltaPathWithoutDeltaCatalog: " + detail); + assertFalse( + pathIoSucceeded, + "Unexpected: Delta PATH I/O succeeded without DeltaCatalog. " + + "Update README spike findings and KD-15 defaults. Detail: " + + detail); + assertTrue( + detail.contains("DELTA_CONFIGURE_SPARK_SESSION") + || detail.toLowerCase().contains("deltacatalog") + || detail.toLowerCase().contains("delta catalog") + || detail.contains("DeltaCatalog"), + "Expected Delta session/catalog configuration error, got: " + detail); + } + + /** + * Iceberg classpath + local I/O smoke using a Hadoop warehouse catalog (no Hive Metastore). Bare + * {@code format("iceberg").save(path)} without catalog conf defaults toward HiveCatalog and fails + * without HMS client jars — document that PATH-style local use still needs catalog/warehouse + * conf. + */ + @Test + @Order(4) + void icebergExtensionIsLoadableAndHadoopWarehouseSmoke() { + assumeIceberg(); + ClassLoader cl = SparkLakeConnectorProbe.class.getClassLoader(); + assertDoesNotThrow( + () -> SparkLakeConnectorProbe.verifyClasspath(Set.of(SparkLakeFormats.FORMAT_ICEBERG), cl)); + + Path warehouse = tempDir.resolve("iceberg_warehouse"); + // Hadoop catalog warehouse must be a URI (file://…); bare paths can fall through to + // HiveCatalog. + String warehouseUri = warehouse.toUri().toString(); + spark = + newSessionBuilder("hop-iceberg-hadoop") + .config(SparkLakeFormats.SPARK_CONF_EXTENSIONS, SparkLakeFormats.ICEBERG_EXTENSIONS) + .config("spark.sql.catalog.local", SparkLakeFormats.ICEBERG_CATALOG) + .config("spark.sql.catalog.local.type", "hadoop") + .config("spark.sql.catalog.local.warehouse", warehouseUri) + .getOrCreate(); + + // Prefer writeTo for Iceberg TABLE-style create (KD-24 may still refine vs saveAsTable). + spark.range(0, 15).toDF("id").writeTo("local.db.sample").using("iceberg").createOrReplace(); + + long countViaTable = spark.table("local.db.sample").count(); + assertEquals(15L, countViaTable); + + // Physical table dir under Hadoop warehouse (metadata written on disk). + Path tableDir = warehouse.resolve("db").resolve("sample"); + assertTrue(tableDir.toFile().isDirectory(), "Expected Iceberg table directory at " + tableDir); + + // Note: bare spark.read.format("iceberg").load(path) still routes through catalog + // resolution and can default to HiveCatalog without HMS jars. Prefer catalog + // identifiers (spark.table / TABLE mode) for v1; path-mode Iceberg needs explicit + // SparkSessionCatalog/Hadoop conf design in PR 3/5. + System.out.println( + "SPIKE_NOTE iceberg: writeTo+createOrReplace + spark.table OK with Hadoop warehouse " + + "(warehouse URI=" + + warehouseUri + + "). saveAsTable not compared — KD-24 / PR 5. " + + "Bare format().save/load(path) without catalog conf defaults toward HiveCatalog."); + } + + @Test + @Order(5) + void bothFormatsProbeWhenPresent() { + assumeDelta(); + assumeIceberg(); + assertDoesNotThrow( + () -> + SparkLakeConnectorProbe.verifyClasspath( + List.of(SparkLakeFormats.FORMAT_DELTA, SparkLakeFormats.FORMAT_ICEBERG))); + } + + private static void assumeDelta() { + assumeTrue( + SparkLakeConnectorProbe.isDeltaPresent(SparkLakeConnectorProbe.class.getClassLoader()), + "Delta connector not on classpath; connectors missing from test classpath"); + } + + private static void assumeIceberg() { + assumeTrue( + SparkLakeConnectorProbe.isIcebergPresent(SparkLakeConnectorProbe.class.getClassLoader()), + "Iceberg connector not on classpath; connectors missing from test classpath"); + } + + private static SparkSession.Builder newSessionBuilder(String appName) { + return SparkSession.builder() + .appName(appName) + .master("local[2]") + .config("spark.ui.enabled", "false") + .config("spark.ui.showConsoleProgress", "false") + .config("spark.metrics.staticSources.enabled", "false") + .config("spark.sql.shuffle.partitions", "2") + .config("spark.driver.host", "localhost") + .config("spark.sql.session.timeZone", "UTC"); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkMaintenanceSqlBuilderTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkMaintenanceSqlBuilderTest.java new file mode 100644 index 00000000000..862f76367d1 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkMaintenanceSqlBuilderTest.java @@ -0,0 +1,171 @@ +/* + * 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.hop.spark.table; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.hop.core.exception.HopException; +import org.junit.jupiter.api.Test; + +class SparkMaintenanceSqlBuilderTest { + + @Test + void deltaOptimize() throws Exception { + String sql = + SparkMaintenanceSqlBuilder.build( + SparkLakeFormats.FORMAT_DELTA, + "delta.`/tmp/t`", + null, + null, + SparkMaintenanceSqlBuilder.OP_OPTIMIZE, + null, + null, + "col1,col2", + null); + assertEquals("OPTIMIZE delta.`/tmp/t` ZORDER BY (col1,col2)", sql); + } + + @Test + void deltaVacuumRequiresRetention() { + assertThrows( + HopException.class, + () -> + SparkMaintenanceSqlBuilder.build( + SparkLakeFormats.FORMAT_DELTA, + "delta.`/tmp/t`", + null, + null, + SparkMaintenanceSqlBuilder.OP_VACUUM, + null, + null, + null, + null)); + } + + @Test + void deltaVacuum() throws Exception { + String sql = + SparkMaintenanceSqlBuilder.build( + SparkLakeFormats.FORMAT_DELTA, + "delta.`/tmp/t`", + null, + null, + SparkMaintenanceSqlBuilder.OP_VACUUM, + "168", + null, + null, + null); + assertEquals("VACUUM delta.`/tmp/t` RETAIN 168 HOURS", sql); + } + + @Test + void icebergRewriteDataFiles() throws Exception { + String sql = + SparkMaintenanceSqlBuilder.build( + SparkLakeFormats.FORMAT_ICEBERG, + "hop_iceberg.`file:///t`", + "hop_iceberg", + "file:///t", + SparkMaintenanceSqlBuilder.OP_OPTIMIZE, + null, + null, + null, + null); + assertTrue(sql.contains("rewrite_data_files")); + assertTrue(sql.contains("hop_iceberg.system")); + assertTrue(sql.contains("file:///t")); + } + + @Test + void icebergExpireSnapshots() throws Exception { + String sql = + SparkMaintenanceSqlBuilder.build( + SparkLakeFormats.FORMAT_ICEBERG, + "lake.db.t", + "lake", + "db.t", + SparkMaintenanceSqlBuilder.OP_EXPIRE_SNAPSHOTS, + "24", + null, + null, + "2"); + assertTrue(sql.contains("expire_snapshots")); + assertTrue(sql.contains("retain_last => 2")); + } + + @Test + void deleteWhereRequiresClause() { + assertThrows( + HopException.class, + () -> + SparkMaintenanceSqlBuilder.build( + SparkLakeFormats.FORMAT_DELTA, + "delta.`/t`", + null, + null, + SparkMaintenanceSqlBuilder.OP_DELETE_WHERE, + null, + "", + null, + null)); + } + + @Test + void deleteWhere() throws Exception { + String sql = + SparkMaintenanceSqlBuilder.build( + SparkLakeFormats.FORMAT_DELTA, + "delta.`/t`", + null, + null, + SparkMaintenanceSqlBuilder.OP_DELETE_WHERE, + null, + "id < 10", + null, + null); + assertEquals("DELETE FROM delta.`/t` WHERE id < 10", sql); + } + + @Test + void destructiveAckFlags() throws Exception { + assertTrue(SparkMaintenanceSqlBuilder.requiresDestructiveAck("VACUUM")); + assertTrue(SparkMaintenanceSqlBuilder.requiresDestructiveAck("EXPIRE_SNAPSHOTS")); + assertTrue(SparkMaintenanceSqlBuilder.requiresDestructiveAck("DELETE_WHERE")); + assertFalse(SparkMaintenanceSqlBuilder.requiresDestructiveAck("OPTIMIZE")); + } + + @Test + void vacuumNotForIceberg() { + assertThrows( + HopException.class, + () -> + SparkMaintenanceSqlBuilder.build( + SparkLakeFormats.FORMAT_ICEBERG, + "x", + "hop_iceberg", + "file:///t", + SparkMaintenanceSqlBuilder.OP_VACUUM, + "1", + null, + null, + null)); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkMergeSqlBuilderTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkMergeSqlBuilderTest.java new file mode 100644 index 00000000000..345965a8e60 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/table/SparkMergeSqlBuilderTest.java @@ -0,0 +1,107 @@ +/* + * 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.hop.spark.table; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.hop.core.exception.HopException; +import org.junit.jupiter.api.Test; + +class SparkMergeSqlBuilderTest { + + @Test + void buildDefaultUpsert() throws Exception { + String sql = + SparkMergeSqlBuilder.build( + "delta.`/tmp/t`", + "hop_merge_src_Upsert", + "t.id = s.id", + SparkMergeSqlBuilder.MATCHED_UPDATE_ALL, + SparkMergeSqlBuilder.NOT_MATCHED_INSERT_ALL, + SparkMergeSqlBuilder.NOT_MATCHED_BY_SOURCE_NONE); + assertTrue(sql.startsWith("MERGE INTO delta.`/tmp/t` AS t")); + assertTrue(sql.contains("USING hop_merge_src_Upsert AS s")); + assertTrue(sql.contains("ON t.id = s.id")); + assertTrue(sql.contains("WHEN MATCHED THEN UPDATE SET *")); + assertTrue(sql.contains("WHEN NOT MATCHED THEN INSERT *")); + assertTrue(!sql.contains("NOT MATCHED BY SOURCE")); + } + + @Test + void buildDeleteMatchedOnly() throws Exception { + String sql = + SparkMergeSqlBuilder.build( + "lake.db.t", + "hop_merge_src_x", + "t.k = s.k", + SparkMergeSqlBuilder.MATCHED_DELETE, + SparkMergeSqlBuilder.NOT_MATCHED_NONE, + SparkMergeSqlBuilder.NOT_MATCHED_BY_SOURCE_NONE); + assertTrue(sql.contains("WHEN MATCHED THEN DELETE")); + assertTrue(!sql.contains("INSERT *")); + } + + @Test + void buildNotMatchedBySourceDelete() throws Exception { + String sql = + SparkMergeSqlBuilder.build( + "delta.`/p`", + "hop_merge_src_a", + "t.id = s.id", + SparkMergeSqlBuilder.MATCHED_UPDATE_ALL, + SparkMergeSqlBuilder.NOT_MATCHED_INSERT_ALL, + SparkMergeSqlBuilder.NOT_MATCHED_BY_SOURCE_DELETE); + assertTrue(sql.contains("WHEN NOT MATCHED BY SOURCE THEN DELETE")); + } + + @Test + void rejectsEmptyCondition() { + assertThrows( + HopException.class, + () -> + SparkMergeSqlBuilder.build( + "t", + "hop_merge_src_a", + "", + SparkMergeSqlBuilder.MATCHED_UPDATE_ALL, + SparkMergeSqlBuilder.NOT_MATCHED_INSERT_ALL, + SparkMergeSqlBuilder.NOT_MATCHED_BY_SOURCE_NONE)); + } + + @Test + void rejectsAllNoneActions() { + assertThrows( + HopException.class, + () -> + SparkMergeSqlBuilder.build( + "t", + "hop_merge_src_a", + "t.id=s.id", + SparkMergeSqlBuilder.MATCHED_NONE, + SparkMergeSqlBuilder.NOT_MATCHED_NONE, + SparkMergeSqlBuilder.NOT_MATCHED_BY_SOURCE_NONE)); + } + + @Test + void sourceViewNameSanitizes() { + assertEquals("hop_merge_src_My_Transform", SparkMergeSqlBuilder.sourceViewName("My Transform")); + assertTrue(SparkMergeSqlBuilder.sourceViewName("123").startsWith("hop_merge_src_")); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/transforms/table/SparkLakeTableInputMetaInjectionTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/transforms/table/SparkLakeTableInputMetaInjectionTest.java new file mode 100644 index 00000000000..96751c07e23 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/transforms/table/SparkLakeTableInputMetaInjectionTest.java @@ -0,0 +1,61 @@ +/* + * 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.hop.spark.transforms.table; + +import java.util.ArrayList; +import java.util.List; +import org.apache.hop.core.injection.BaseMetadataInjectionTestJunit5; +import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension; +import org.apache.hop.spark.transforms.io.SparkField; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SparkLakeTableInputMetaInjectionTest + extends BaseMetadataInjectionTestJunit5 { + + @RegisterExtension + static RestoreHopEngineEnvironmentExtension env = new RestoreHopEngineEnvironmentExtension(); + + @BeforeEach + void setup() throws Exception { + SparkLakeTableInputMeta meta = new SparkLakeTableInputMeta(); + List fields = new ArrayList<>(); + fields.add(new SparkField()); + meta.setFields(fields); + setup(meta); + } + + @Test + void test() throws Exception { + check("FORMAT", () -> meta.getFormat()); + check("IDENTIFIER_MODE", () -> meta.getIdentifierMode()); + check("TABLE_PATH", () -> meta.getTablePath()); + check("TABLE_IDENTIFIER", () -> meta.getTableIdentifier()); + check("CATALOG_METADATA_NAME", () -> meta.getCatalogMetadataName()); + check("TIME_TRAVEL_TYPE", () -> meta.getTimeTravelType()); + check("TIME_TRAVEL_VERSION", () -> meta.getTimeTravelVersion()); + check("TIME_TRAVEL_TIMESTAMP", () -> meta.getTimeTravelTimestamp()); + check("EXTRA_OPTIONS", () -> meta.getExtraOptions()); + check("NAME", () -> meta.getFields().get(0).getName()); + check("TYPE", () -> meta.getFields().get(0).getHopType()); + check("LENGTH", () -> meta.getFields().get(0).getLength()); + check("PRECISION", () -> meta.getFields().get(0).getPrecision()); + check("FORMAT_MASK", () -> meta.getFields().get(0).getFormatMask()); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/transforms/table/SparkLakeTableMaintenanceMetaInjectionTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/transforms/table/SparkLakeTableMaintenanceMetaInjectionTest.java new file mode 100644 index 00000000000..abb2adcf80b --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/transforms/table/SparkLakeTableMaintenanceMetaInjectionTest.java @@ -0,0 +1,51 @@ +/* + * 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.hop.spark.transforms.table; + +import org.apache.hop.core.injection.BaseMetadataInjectionTestJunit5; +import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SparkLakeTableMaintenanceMetaInjectionTest + extends BaseMetadataInjectionTestJunit5 { + + @RegisterExtension + static RestoreHopEngineEnvironmentExtension env = new RestoreHopEngineEnvironmentExtension(); + + @BeforeEach + void setup() throws Exception { + setup(new SparkLakeTableMaintenanceMeta()); + } + + @Test + void test() throws Exception { + check("FORMAT", () -> meta.getFormat()); + check("IDENTIFIER_MODE", () -> meta.getIdentifierMode()); + check("TABLE_PATH", () -> meta.getTablePath()); + check("TABLE_IDENTIFIER", () -> meta.getTableIdentifier()); + check("CATALOG_METADATA_NAME", () -> meta.getCatalogMetadataName()); + check("OPERATION", () -> meta.getOperation()); + check("RETENTION_HOURS", () -> meta.getRetentionHours()); + check("RETAIN_LAST", () -> meta.getRetainLast()); + check("WHERE_CLAUSE", () -> meta.getWhereClause()); + check("ZORDER_COLUMNS", () -> meta.getZOrderColumns()); + check("ACKNOWLEDGE_DESTRUCTIVE", () -> meta.isAcknowledgeDestructive()); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/transforms/table/SparkLakeTableMergeMetaInjectionTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/transforms/table/SparkLakeTableMergeMetaInjectionTest.java new file mode 100644 index 00000000000..23183d79795 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/transforms/table/SparkLakeTableMergeMetaInjectionTest.java @@ -0,0 +1,50 @@ +/* + * 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.hop.spark.transforms.table; + +import org.apache.hop.core.injection.BaseMetadataInjectionTestJunit5; +import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SparkLakeTableMergeMetaInjectionTest + extends BaseMetadataInjectionTestJunit5 { + + @RegisterExtension + static RestoreHopEngineEnvironmentExtension env = new RestoreHopEngineEnvironmentExtension(); + + @BeforeEach + void setup() throws Exception { + setup(new SparkLakeTableMergeMeta()); + } + + @Test + void test() throws Exception { + check("FORMAT", () -> meta.getFormat()); + check("IDENTIFIER_MODE", () -> meta.getIdentifierMode()); + check("TABLE_PATH", () -> meta.getTablePath()); + check("TABLE_IDENTIFIER", () -> meta.getTableIdentifier()); + check("CATALOG_METADATA_NAME", () -> meta.getCatalogMetadataName()); + check("MERGE_CONDITION", () -> meta.getMergeCondition()); + check("MATCHED_ACTION", () -> meta.getMatchedAction()); + check("NOT_MATCHED_ACTION", () -> meta.getNotMatchedAction()); + check("NOT_MATCHED_BY_SOURCE_ACTION", () -> meta.getNotMatchedBySourceAction()); + check("RAW_MERGE_SQL", () -> meta.getRawMergeSql()); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/transforms/table/SparkLakeTableOutputMetaInjectionTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/transforms/table/SparkLakeTableOutputMetaInjectionTest.java new file mode 100644 index 00000000000..2dc9f607409 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/transforms/table/SparkLakeTableOutputMetaInjectionTest.java @@ -0,0 +1,49 @@ +/* + * 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.hop.spark.transforms.table; + +import org.apache.hop.core.injection.BaseMetadataInjectionTestJunit5; +import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class SparkLakeTableOutputMetaInjectionTest + extends BaseMetadataInjectionTestJunit5 { + + @RegisterExtension + static RestoreHopEngineEnvironmentExtension env = new RestoreHopEngineEnvironmentExtension(); + + @BeforeEach + void setup() throws Exception { + setup(new SparkLakeTableOutputMeta()); + } + + @Test + void test() throws Exception { + check("FORMAT", () -> meta.getFormat()); + check("IDENTIFIER_MODE", () -> meta.getIdentifierMode()); + check("TABLE_PATH", () -> meta.getTablePath()); + check("TABLE_IDENTIFIER", () -> meta.getTableIdentifier()); + check("CATALOG_METADATA_NAME", () -> meta.getCatalogMetadataName()); + check("SAVE_MODE", () -> meta.getSaveMode()); + check("PARTITION_BY", () -> meta.getPartitionByColumns()); + check("COALESCE", () -> meta.getCoalescePartitions()); + check("EXTRA_OPTIONS", () -> meta.getExtraOptions()); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/util/SparkPathDialectTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/util/SparkPathDialectTest.java new file mode 100644 index 00000000000..b00e3597de3 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/util/SparkPathDialectTest.java @@ -0,0 +1,130 @@ +/* + * 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.hop.spark.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; +import org.apache.hop.spark.engines.SparkPipelineRunConfiguration; +import org.junit.jupiter.api.Test; + +class SparkPathDialectTest { + + @Test + void extractSchemeFromCommonUris() { + assertEquals("s3", SparkPathDialect.extractScheme("s3://bucket/key")); + assertEquals("s3a", SparkPathDialect.extractScheme("s3a://bucket/key")); + assertEquals("hdfs", SparkPathDialect.extractScheme("hdfs://nn:8020/path")); + assertEquals("file", SparkPathDialect.extractScheme("file:///tmp/x")); + assertEquals("file", SparkPathDialect.extractScheme("file:/tmp/x")); + assertEquals("azure", SparkPathDialect.extractScheme("AZURE://account/container")); + assertNull(SparkPathDialect.extractScheme("/local/path")); + assertNull(SparkPathDialect.extractScheme("relative/path")); + assertNull(SparkPathDialect.extractScheme(null)); + assertNull(SparkPathDialect.extractScheme("")); + } + + @Test + void hopVfsSchemesDetected() { + assertTrue(SparkPathDialect.isKnownHopVfsScheme("s3://bucket/a")); + assertTrue(SparkPathDialect.isKnownHopVfsScheme("azure://x")); + assertTrue(SparkPathDialect.isKnownHopVfsScheme("azfs://x")); + assertTrue(SparkPathDialect.isKnownHopVfsScheme("googledrive://x")); + assertTrue(SparkPathDialect.isKnownHopVfsScheme("dropbox://x")); + assertTrue(SparkPathDialect.isKnownHopVfsScheme("webdav4://host/path")); + assertTrue(SparkPathDialect.isKnownHopVfsScheme("webdav4s://host/path")); + } + + @Test + void sparkSchemesNotFlaggedAsHopVfs() { + assertFalse(SparkPathDialect.isKnownHopVfsScheme("s3a://bucket/a")); + assertFalse(SparkPathDialect.isKnownHopVfsScheme("hdfs://nn/path")); + assertFalse(SparkPathDialect.isKnownHopVfsScheme("file:///tmp/x")); + assertFalse(SparkPathDialect.isKnownHopVfsScheme("abfs://container@account/path")); + assertFalse(SparkPathDialect.isKnownHopVfsScheme("gs://bucket/obj")); + assertFalse(SparkPathDialect.isKnownHopVfsScheme("${PROJECT_HOME}/out")); + // Named MinIO-style schemes cannot be listed exhaustively + assertFalse(SparkPathDialect.isKnownHopVfsScheme("minio:///demo/file")); + } + + @Test + void s3HintMentionsS3a() { + String hint = SparkPathDialect.hopVfsSchemeHint("s3://bucket/key"); + assertNotNull(hint); + assertTrue(hint.contains("s3a://")); + assertTrue(hint.toLowerCase().contains("hop vfs")); + } + + @Test + void withPathHintAppendsOnlyForHopSchemes() { + String base = "Error reading 's3://b/k' as csv"; + String with = SparkPathDialect.withPathHint(base, "s3://b/k"); + assertTrue(with.startsWith(base)); + assertTrue(with.contains("Hint:")); + assertTrue(with.contains("s3a://")); + + assertEquals( + "Error reading 's3a://b/k' as csv", + SparkPathDialect.withPathHint("Error reading 's3a://b/k' as csv", "s3a://b/k")); + } + + @Test + void parseSchemeMapIgnoresCommentsAndNormalizesTokens() { + Map map = + SparkPathDialect.parseSchemeMap( + """ + # hop to spark + s3=s3a + s3://=s3a:// + minio = s3a + azure=abfs + broken + =s3a + s3a= + """); + assertEquals("s3a", map.get("s3")); // last s3:// line wins over s3= + assertEquals("s3a", map.get("minio")); + assertEquals("abfs", map.get("azure")); + assertFalse(map.containsKey("s3a")); + } + + @Test + void toSparkUriRewritesMappedSchemesOnly() { + String map = "s3=s3a\nminio=s3a\n"; + assertEquals("s3a://bucket/key", SparkPathDialect.toSparkUri("s3://bucket/key", map)); + assertEquals("s3a://bucket/key", SparkPathDialect.toSparkUri("S3://bucket/key", map)); + assertEquals("s3a:///demo/x", SparkPathDialect.toSparkUri("minio:///demo/x", map)); + assertEquals("s3a://bucket/key", SparkPathDialect.toSparkUri("s3a://bucket/key", map)); + assertEquals("/local/path", SparkPathDialect.toSparkUri("/local/path", map)); + assertEquals("hdfs://nn/path", SparkPathDialect.toSparkUri("hdfs://nn/path", map)); + assertNull(SparkPathDialect.toSparkUri(null, map)); + assertEquals("", SparkPathDialect.toSparkUri("", map)); + } + + @Test + void toSparkUriWithRunConfiguration() { + SparkPipelineRunConfiguration config = new SparkPipelineRunConfiguration(); + config.setPathSchemeMap("s3=s3a"); + assertEquals("s3a://b/k", SparkPathDialect.toSparkUri("s3://b/k", config)); + assertEquals("s3://b/k", SparkPathDialect.toSparkUri("s3://b/k", (String) null)); + } +} diff --git a/plugins/engines/spark/src/test/java/org/apache/hop/spark/util/SparkRunModeTest.java b/plugins/engines/spark/src/test/java/org/apache/hop/spark/util/SparkRunModeTest.java new file mode 100644 index 00000000000..95ca4daa6b5 --- /dev/null +++ b/plugins/engines/spark/src/test/java/org/apache/hop/spark/util/SparkRunModeTest.java @@ -0,0 +1,92 @@ +/* + * 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.hop.spark.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.hop.pipeline.transform.TransformMeta; +import org.apache.hop.spark.engines.SparkPipelineRunConfiguration; +import org.junit.jupiter.api.Test; + +class SparkRunModeTest { + + @Test + void parseConfigValueDefaultsToDistributed() { + assertEquals(SparkRunMode.DISTRIBUTED, SparkRunMode.parseConfigValue(null)); + assertEquals(SparkRunMode.DISTRIBUTED, SparkRunMode.parseConfigValue("")); + assertEquals(SparkRunMode.DISTRIBUTED, SparkRunMode.parseConfigValue("DISTRIBUTED")); + assertEquals(SparkRunMode.DRIVER_ONLY, SparkRunMode.parseConfigValue("DRIVER_ONLY")); + assertEquals(SparkRunMode.DRIVER_ONLY, SparkRunMode.parseConfigValue("driver_only")); + } + + @Test + void resolveInheritsRunConfiguration() { + TransformMeta transform = new TransformMeta(); + SparkPipelineRunConfiguration config = new SparkPipelineRunConfiguration(); + assertEquals(SparkRunMode.DISTRIBUTED, SparkRunMode.resolve(transform, config)); + + config.setGenericTransformRunMode(SparkRunMode.DRIVER_ONLY.name()); + assertEquals(SparkRunMode.DRIVER_ONLY, SparkRunMode.resolve(transform, config)); + } + + @Test + void forceOverrideBeatsRunConfiguration() { + TransformMeta transform = new TransformMeta(); + SparkPipelineRunConfiguration config = new SparkPipelineRunConfiguration(); + config.setGenericTransformRunMode(SparkRunMode.DRIVER_ONLY.name()); + + SparkRunMode.setOverride(transform, SparkRunMode.OVERRIDE_FORCE_DISTRIBUTED); + assertEquals(SparkRunMode.DISTRIBUTED, SparkRunMode.resolve(transform, config)); + assertTrue(SparkRunMode.hasForcedOverride(transform)); + + SparkRunMode.setOverride(transform, SparkRunMode.OVERRIDE_FORCE_DRIVER_ONLY); + config.setGenericTransformRunMode(SparkRunMode.DISTRIBUTED.name()); + assertEquals(SparkRunMode.DRIVER_ONLY, SparkRunMode.resolve(transform, config)); + } + + @Test + void inheritClearsAttribute() { + TransformMeta transform = new TransformMeta(); + SparkRunMode.setOverride(transform, SparkRunMode.OVERRIDE_FORCE_DRIVER_ONLY); + assertEquals( + SparkRunMode.OVERRIDE_FORCE_DRIVER_ONLY, + transform.getAttribute(SparkRunMode.ATTRIBUTE_GROUP, SparkRunMode.ATTRIBUTE_KEY)); + + SparkRunMode.setOverride(transform, SparkRunMode.OVERRIDE_INHERIT); + assertFalse(SparkRunMode.hasForcedOverride(transform)); + assertEquals(SparkRunMode.OVERRIDE_INHERIT, SparkRunMode.getOverride(transform)); + } + + @Test + void displayLabelRoundTrip() { + assertEquals( + SparkRunMode.OVERRIDE_FORCE_DISTRIBUTED, + SparkRunMode.overrideFromDisplayLabel(SparkRunMode.DISPLAY_FORCE_DISTRIBUTED)); + assertEquals( + SparkRunMode.OVERRIDE_FORCE_DRIVER_ONLY, + SparkRunMode.overrideFromDisplayLabel(SparkRunMode.DISPLAY_FORCE_DRIVER_ONLY)); + assertEquals( + SparkRunMode.OVERRIDE_INHERIT, + SparkRunMode.overrideFromDisplayLabel(SparkRunMode.DISPLAY_INHERIT)); + assertEquals( + SparkRunMode.DISPLAY_FORCE_DRIVER_ONLY, + SparkRunMode.displayLabelForOverride(SparkRunMode.OVERRIDE_FORCE_DRIVER_ONLY)); + } +} diff --git a/plugins/tech/cassandra/src/test/java/org/apache/hop/pipeline/transforms/cassandrasstableoutput/writer/Cql3SsTableWriterTest.java b/plugins/tech/cassandra/src/test/java/org/apache/hop/pipeline/transforms/cassandrasstableoutput/writer/Cql3SsTableWriterTest.java index f38b631b6e7..a43d8d9857e 100644 --- a/plugins/tech/cassandra/src/test/java/org/apache/hop/pipeline/transforms/cassandrasstableoutput/writer/Cql3SsTableWriterTest.java +++ b/plugins/tech/cassandra/src/test/java/org/apache/hop/pipeline/transforms/cassandrasstableoutput/writer/Cql3SsTableWriterTest.java @@ -35,11 +35,13 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.apache.cassandra.io.sstable.CQLSSTableWriter; import org.apache.commons.lang3.SystemUtils; +import org.apache.hop.core.exception.HopException; import org.apache.hop.core.exception.HopPluginException; import org.apache.hop.core.row.IRowMeta; import org.apache.hop.core.row.IValueMeta; import org.apache.hop.core.row.value.ValueMetaBase; import org.apache.hop.core.row.value.ValueMetaFactory; +import org.apache.hop.core.util.TestUtil; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.mockito.stubbing.Answer; @@ -50,6 +52,11 @@ static void notOnWindows() { assumeFalse(SystemUtils.IS_OS_WINDOWS); } + @BeforeAll + static void setUp() throws HopException { + TestUtil.registerTestPluginTypes(); + } + public static final String KEY_FIELD = "KEY_FIELD"; public static final String TABLE = "TABLE"; public static final String KEY_SPACE = "KEY_SPACE"; diff --git a/plugins/tech/databricks/pom.xml b/plugins/tech/databricks/pom.xml index 8c94d47c1b6..895ad733b09 100644 --- a/plugins/tech/databricks/pom.xml +++ b/plugins/tech/databricks/pom.xml @@ -29,4 +29,14 @@ jar Hop Plugins Technology Databricks + + + + com.github.tomakehurst + wiremock-jre8-standalone + 2.35.2 + test + + + diff --git a/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/client/DatabricksJobsClient.java b/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/client/DatabricksJobsClient.java new file mode 100644 index 00000000000..8c516622425 --- /dev/null +++ b/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/client/DatabricksJobsClient.java @@ -0,0 +1,77 @@ +/* + * 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.hop.databricks.client; + +import java.util.Map; +import org.apache.hop.core.exception.HopException; + +/** + * Minimal Databricks Jobs API surface used by Hop. Implementations may use the official SDK or + * REST. + */ +public interface DatabricksJobsClient extends AutoCloseable { + + /** + * Verify credentials against the workspace. Returns a short identity string for UI (user name or + * host). + */ + String testConnection() throws HopException; + + /** Trigger a run of an existing job; returns the new run id. */ + long runNow(long jobId, Map notebookOrJarParams) throws HopException; + + /** One-time submit (runs/submit). Body is raw Jobs API JSON. Returns run id. */ + long submitRun(String submitRunJsonBody) throws HopException; + + /** Create a job from raw Jobs API create JSON. Returns job id. */ + long createJob(String createJobJsonBody) throws HopException; + + /** Reset/update a job (jobs/reset) with raw JSON including job_id. */ + void resetJob(String resetJobJsonBody) throws HopException; + + DatabricksRunStatus getRun(long runId) throws HopException; + + void cancelRun(long runId) throws HopException; + + /** + * Upload a local file to a workspace path and overwrite if present. + * + *

Paths under {@code /Volumes/…} (Unity Catalog volumes) or {@code /Workspace/…} use the Files + * API ({@code PUT /api/2.0/fs/files/…}). Classic {@code dbfs:/…} / {@code /FileStore/…} paths use + * the legacy DBFS API (create / add-block / close). + */ + void uploadToDbfs(java.nio.file.Path localFile, String dbfsPath) throws HopException; + + /** + * Metadata for a remote workspace file (Files API or DBFS). {@link + * WorkspaceFileMetadata#exists()} is false when the path is missing or inaccessible as a file. + */ + WorkspaceFileMetadata getFileMetadata(String workspacePath) throws HopException; + + /** + * Download a small remote text file (e.g. checksum sidecar). Returns empty when the file does not + * exist. + */ + java.util.Optional downloadTextIfExists(String workspacePath) throws HopException; + + /** Upload UTF-8 text to a workspace path (overwrites). Convenience for tiny sidecar files. */ + void uploadText(String workspacePath, String text) throws HopException; + + @Override + void close(); +} diff --git a/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/client/DatabricksRunLifeCycleState.java b/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/client/DatabricksRunLifeCycleState.java new file mode 100644 index 00000000000..38409400ded --- /dev/null +++ b/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/client/DatabricksRunLifeCycleState.java @@ -0,0 +1,51 @@ +/* + * 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.hop.databricks.client; + +/** + * Subset of Databricks Jobs run life_cycle_state values used by Hop. Unknown API values map to + * {@link #UNKNOWN}. + */ +public enum DatabricksRunLifeCycleState { + PENDING, + RUNNING, + TERMINATING, + TERMINATED, + SKIPPED, + INTERNAL_ERROR, + BLOCKED, + WAITING_FOR_RETRY, + QUEUED, + UNKNOWN; + + public static DatabricksRunLifeCycleState fromApi(String value) { + if (value == null || value.isBlank()) { + return UNKNOWN; + } + try { + return DatabricksRunLifeCycleState.valueOf(value.trim().toUpperCase()); + } catch (IllegalArgumentException e) { + return UNKNOWN; + } + } + + /** True when the run will not change further (success, failure, or skip). */ + public boolean isTerminal() { + return this == TERMINATED || this == SKIPPED || this == INTERNAL_ERROR; + } +} diff --git a/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/client/DatabricksRunStatus.java b/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/client/DatabricksRunStatus.java new file mode 100644 index 00000000000..a87cf7f3a26 --- /dev/null +++ b/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/client/DatabricksRunStatus.java @@ -0,0 +1,97 @@ +/* + * 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.hop.databricks.client; + +import java.util.Objects; + +/** Snapshot of a Databricks job run. */ +public final class DatabricksRunStatus { + + private final long runId; + private final Long jobId; + private final DatabricksRunLifeCycleState lifeCycleState; + private final String resultState; + private final String stateMessage; + private final String runPageUrl; + + public DatabricksRunStatus( + long runId, + Long jobId, + DatabricksRunLifeCycleState lifeCycleState, + String resultState, + String stateMessage, + String runPageUrl) { + this.runId = runId; + this.jobId = jobId; + this.lifeCycleState = + Objects.requireNonNullElse(lifeCycleState, DatabricksRunLifeCycleState.UNKNOWN); + this.resultState = resultState; + this.stateMessage = stateMessage; + this.runPageUrl = runPageUrl; + } + + public long getRunId() { + return runId; + } + + public Long getJobId() { + return jobId; + } + + public DatabricksRunLifeCycleState getLifeCycleState() { + return lifeCycleState; + } + + /** e.g. SUCCESS, FAILED, TIMEDOUT, CANCELED when terminated. */ + public String getResultState() { + return resultState; + } + + public String getStateMessage() { + return stateMessage; + } + + public String getRunPageUrl() { + return runPageUrl; + } + + public boolean isSuccess() { + return lifeCycleState == DatabricksRunLifeCycleState.TERMINATED + && "SUCCESS".equalsIgnoreCase(resultState); + } + + public boolean isFailed() { + if (lifeCycleState == DatabricksRunLifeCycleState.INTERNAL_ERROR) { + return true; + } + if (lifeCycleState != DatabricksRunLifeCycleState.TERMINATED) { + return false; + } + return resultState != null + && !"SUCCESS".equalsIgnoreCase(resultState) + && !"CANCELED".equalsIgnoreCase(resultState); + } + + /** Compact status string for Hop variables. */ + public String toStatusVariable() { + if (lifeCycleState == DatabricksRunLifeCycleState.TERMINATED && resultState != null) { + return resultState.toUpperCase(); + } + return lifeCycleState.name(); + } +} diff --git a/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/client/DatabricksRunWaiter.java b/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/client/DatabricksRunWaiter.java new file mode 100644 index 00000000000..d5de7df9b70 --- /dev/null +++ b/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/client/DatabricksRunWaiter.java @@ -0,0 +1,109 @@ +/* + * 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.hop.databricks.client; + +import org.apache.hop.core.Const; +import org.apache.hop.core.exception.HopException; + +/** + * Polls {@link DatabricksJobsClient#getRun(long)} until the run is terminal, timed out, or the + * workflow is stopped. Shared by Job Run and Job Wait actions. + */ +public final class DatabricksRunWaiter { + + @FunctionalInterface + public interface StatusListener { + void onStatus(DatabricksRunStatus status); + } + + public interface Hooks { + /** True when the parent workflow was stopped. */ + boolean isStopped(); + + void onStatus(DatabricksRunStatus status); + + void logDetailed(String message); + + void logError(String message); + } + + private DatabricksRunWaiter() {} + + /** + * @param timeoutSec 0 or negative = no timeout + * @param pollSec minimum 1 second between polls + * @param cancelOnStop if true, call cancelRun when stopped or on timeout + */ + public static DatabricksRunStatus waitFor( + DatabricksJobsClient client, + long runId, + int timeoutSec, + int pollSec, + boolean cancelOnStop, + Hooks hooks) + throws HopException, InterruptedException { + int poll = Math.max(1, pollSec); + long deadline = + timeoutSec <= 0 ? Long.MAX_VALUE : System.currentTimeMillis() + timeoutSec * 1000L; + + while (true) { + if (hooks != null && hooks.isStopped()) { + if (cancelOnStop) { + tryCancel(client, runId, hooks); + } + throw new HopException("Workflow was stopped while waiting for Databricks run " + runId); + } + + DatabricksRunStatus status = client.getRun(runId); + if (hooks != null) { + hooks.onStatus(status); + hooks.logDetailed( + "Databricks run " + + runId + + " state=" + + status.getLifeCycleState() + + " result=" + + Const.NVL(status.getResultState(), "-")); + } + + if (status.getLifeCycleState().isTerminal()) { + return status; + } + + if (System.currentTimeMillis() >= deadline) { + if (cancelOnStop) { + tryCancel(client, runId, hooks); + } + throw new HopException( + "Timed out after " + timeoutSec + " seconds waiting for Databricks run " + runId); + } + + Thread.sleep(poll * 1000L); + } + } + + private static void tryCancel(DatabricksJobsClient client, long runId, Hooks hooks) { + try { + client.cancelRun(runId); + } catch (Exception e) { + if (hooks != null) { + hooks.logError("Failed to cancel Databricks run " + runId + ": " + e.getMessage()); + } + } + } +} diff --git a/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/client/RestDatabricksJobsClient.java b/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/client/RestDatabricksJobsClient.java new file mode 100644 index 00000000000..8137b30b477 --- /dev/null +++ b/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/client/RestDatabricksJobsClient.java @@ -0,0 +1,697 @@ +/* + * 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.hop.databricks.client; + +import java.io.InputStream; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.encryption.Encr; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.databricks.metadata.DatabricksConnection; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; + +/** + * Databricks Jobs API client using {@link HttpClient} and Jobs REST endpoints (default base {@code + * /api/2.1}). Does not log tokens. Uploads use the Files API for UC Volumes / Workspace paths and + * the legacy DBFS API for classic {@code dbfs:/} roots. + */ +public final class RestDatabricksJobsClient implements DatabricksJobsClient { + + private static final Duration TIMEOUT = Duration.ofSeconds(60); + private static final Duration UPLOAD_TIMEOUT = Duration.ofMinutes(30); + + /** DBFS add-block max is 1 MiB of base64-decoded data. */ + private static final int DBFS_BLOCK_BYTES = 1024 * 1024; + + /** Files API single PUT supports files up to 5 GiB. */ + private static final String FILES_API_PREFIX = "/api/2.0/fs/files"; + + private final String hostBase; + private final String apiBase; + private final String token; + private final HttpClient httpClient; + private final JSONParser parser = new JSONParser(); + + public RestDatabricksJobsClient( + String hostBase, String apiBase, String token, HttpClient httpClient) { + this.hostBase = normalizeHost(hostBase); + this.apiBase = normalizeApiBase(apiBase); + this.token = Objects.requireNonNull(token, "token"); + this.httpClient = Objects.requireNonNull(httpClient, "httpClient"); + } + + public static RestDatabricksJobsClient create( + DatabricksConnection connection, IVariables variables) throws HopException { + if (connection == null) { + throw new HopException("Databricks connection is required"); + } + String host = resolve(variables, connection.getHost()); + // PAT may be a literal, ${variable}, or Encrypted… (incl. after variable resolve) + String token = + Encr.decryptPasswordOptionallyEncrypted(resolve(variables, connection.getToken())); + String apiBase = resolve(variables, connection.getApiBasePath()); + if (StringUtils.isBlank(host)) { + throw new HopException("Databricks workspace host is required"); + } + if (StringUtils.isBlank(token)) { + throw new HopException("Databricks personal access token is required"); + } + HttpClient client = HttpClient.newBuilder().connectTimeout(TIMEOUT).build(); + return new RestDatabricksJobsClient(host, apiBase, token, client); + } + + /** Visible for tests with a custom {@link HttpClient}. */ + public static RestDatabricksJobsClient createForTest( + String hostBase, String apiBase, String token, HttpClient httpClient) { + return new RestDatabricksJobsClient(hostBase, apiBase, token, httpClient); + } + + @Override + public String testConnection() throws HopException { + // Prefer SCIM me when available; fall back to listing one job. + try { + String body = get("/api/2.0/preview/scim/v2/Me"); + JSONObject json = parseObject(body); + Object userName = json.get("userName"); + if (userName != null) { + return userName.toString(); + } + Object display = json.get("displayName"); + if (display != null) { + return display.toString(); + } + } catch (HopException ignored) { + // SCIM may be disabled; use jobs/list + } + String body = get(apiBase + "/jobs/list?limit=1"); + parseObject(body); // validates JSON / auth + return hostBase; + } + + @Override + public long runNow(long jobId, Map notebookOrJarParams) throws HopException { + JSONObject body = new JSONObject(); + body.put("job_id", jobId); + if (notebookOrJarParams != null && !notebookOrJarParams.isEmpty()) { + // Jobs API: notebook_params / jar_params / python_params — use notebook_params as generic map + JSONObject params = new JSONObject(); + params.putAll(notebookOrJarParams); + body.put("notebook_params", params); + } + String response = postJson(apiBase + "/jobs/run-now", body.toJSONString()); + JSONObject json = parseObject(response); + return requireLong(json, "run_id"); + } + + @Override + public long submitRun(String submitRunJsonBody) throws HopException { + String response = postJson(apiBase + "/jobs/runs/submit", submitRunJsonBody); + JSONObject json = parseObject(response); + return requireLong(json, "run_id"); + } + + @Override + public long createJob(String createJobJsonBody) throws HopException { + String response = postJson(apiBase + "/jobs/create", createJobJsonBody); + JSONObject json = parseObject(response); + return requireLong(json, "job_id"); + } + + @Override + public void resetJob(String resetJobJsonBody) throws HopException { + postJson(apiBase + "/jobs/reset", resetJobJsonBody); + } + + @Override + public DatabricksRunStatus getRun(long runId) throws HopException { + String response = + get( + apiBase + + "/jobs/runs/get?run_id=" + + URLEncoder.encode(Long.toString(runId), StandardCharsets.UTF_8)); + JSONObject json = parseObject(response); + long id = requireLong(json, "run_id"); + Long jobId = null; + if (json.get("job_id") != null) { + jobId = ((Number) json.get("job_id")).longValue(); + } + String pageUrl = json.get("run_page_url") != null ? json.get("run_page_url").toString() : null; + JSONObject state = (JSONObject) json.get("state"); + DatabricksRunLifeCycleState life = DatabricksRunLifeCycleState.UNKNOWN; + String resultState = null; + String stateMessage = null; + if (state != null) { + if (state.get("life_cycle_state") != null) { + life = DatabricksRunLifeCycleState.fromApi(state.get("life_cycle_state").toString()); + } + if (state.get("result_state") != null) { + resultState = state.get("result_state").toString(); + } + if (state.get("state_message") != null) { + stateMessage = state.get("state_message").toString(); + } + } + return new DatabricksRunStatus(id, jobId, life, resultState, stateMessage, pageUrl); + } + + @Override + public void cancelRun(long runId) throws HopException { + JSONObject body = new JSONObject(); + body.put("run_id", runId); + postJson(apiBase + "/jobs/runs/cancel", body.toJSONString()); + } + + @Override + public void uploadToDbfs(Path localFile, String dbfsPath) throws HopException { + if (localFile == null || !Files.isRegularFile(localFile)) { + throw new HopException("Local file for workspace upload does not exist: " + localFile); + } + String path = normalizeDbfsPath(dbfsPath); + if (isFilesApiPath(path)) { + uploadViaFilesApi(localFile, path); + } else { + uploadViaDbfsApi(localFile, path); + } + } + + @Override + public WorkspaceFileMetadata getFileMetadata(String workspacePath) throws HopException { + String path = normalizeDbfsPath(workspacePath); + if (isFilesApiPath(path)) { + return getFileMetadataFilesApi(path); + } + return getFileMetadataDbfs(path); + } + + @Override + public Optional downloadTextIfExists(String workspacePath) throws HopException { + String path = normalizeDbfsPath(workspacePath); + if (isFilesApiPath(path)) { + return downloadTextFilesApi(path); + } + return downloadTextDbfs(path); + } + + @Override + public void uploadText(String workspacePath, String text) throws HopException { + if (text == null) { + text = ""; + } + try { + Path tmp = Files.createTempFile("hop-dbx-text-", ".txt"); + try { + Files.writeString(tmp, text, StandardCharsets.UTF_8); + uploadToDbfs(tmp, workspacePath); + } finally { + Files.deleteIfExists(tmp); + } + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException("Failed to upload text to " + workspacePath, e); + } + } + + /** + * UC Volumes and Workspace files must use the Files API. Classic DBFS roots (FileStore, etc.) use + * the legacy DBFS block API. + */ + static boolean isFilesApiPath(String absolutePath) { + if (StringUtils.isBlank(absolutePath)) { + return false; + } + String p = absolutePath.trim(); + return p.startsWith("/Volumes/") + || p.equals("/Volumes") + || p.startsWith("/Workspace/") + || p.equals("/Workspace"); + } + + /** + * Upload via {@code PUT /api/2.0/fs/files{path}?overwrite=true} (UC Volumes / Workspace). Body is + * raw octets; max ~5 GiB per Databricks Files API. + */ + private void uploadViaFilesApi(Path localFile, String absolutePath) throws HopException { + try { + String encodedPath = encodeFilesApiPath(absolutePath); + String url = hostBase + FILES_API_PREFIX + encodedPath + "?overwrite=true"; + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(UPLOAD_TIMEOUT) + .header("Authorization", "Bearer " + token) + .header("Content-Type", "application/octet-stream") + .PUT(HttpRequest.BodyPublishers.ofFile(localFile)) + .build(); + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + int code = response.statusCode(); + String body = response.body() == null ? "" : response.body(); + if (code < 200 || code >= 300) { + throw new HopException( + "Databricks API HTTP " + + code + + " for PUT " + + FILES_API_PREFIX + + absolutePath + + ": " + + sanitizeError(body)); + } + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException( + "Failed to upload " + localFile + " to " + absolutePath + " via Files API", e); + } + } + + /** + * Files API get-metadata: HEAD {@code /api/2.0/fs/files{path}} — size from Content-Length (no + * body). + */ + private WorkspaceFileMetadata getFileMetadataFilesApi(String absolutePath) throws HopException { + try { + String encodedPath = encodeFilesApiPath(absolutePath); + String url = hostBase + FILES_API_PREFIX + encodedPath; + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(TIMEOUT) + .header("Authorization", "Bearer " + token) + .method("HEAD", HttpRequest.BodyPublishers.noBody()) + .build(); + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.discarding()); + int code = response.statusCode(); + if (code == 404) { + return WorkspaceFileMetadata.missing(); + } + if (code < 200 || code >= 300) { + // Some gateways reject HEAD — fall back to GET with range 0-0 for size + return getFileMetadataFilesApiGetHeaders(absolutePath); + } + long size = contentLength(response.headers().firstValue("Content-Length").orElse(null)); + if (size < 0) { + size = contentLength(response.headers().firstValue("content-length").orElse(null)); + } + if (size < 0) { + return getFileMetadataFilesApiGetHeaders(absolutePath); + } + return WorkspaceFileMetadata.ofFile(size); + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException("Failed to get metadata for " + absolutePath, e); + } + } + + private WorkspaceFileMetadata getFileMetadataFilesApiGetHeaders(String absolutePath) + throws HopException { + try { + String encodedPath = encodeFilesApiPath(absolutePath); + String url = hostBase + FILES_API_PREFIX + encodedPath; + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(TIMEOUT) + .header("Authorization", "Bearer " + token) + .header("Range", "bytes=0-0") + .GET() + .build(); + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray()); + int code = response.statusCode(); + if (code == 404) { + return WorkspaceFileMetadata.missing(); + } + if (code != 200 && code != 206) { + String errBody = + response.body() == null ? "" : new String(response.body(), StandardCharsets.UTF_8); + throw new HopException( + "Databricks API HTTP " + + code + + " for GET " + + FILES_API_PREFIX + + absolutePath + + ": " + + sanitizeError(errBody)); + } + Optional contentRange = response.headers().firstValue("Content-Range"); + if (contentRange.isPresent()) { + // bytes 0-0/12345 + String cr = contentRange.get(); + int slash = cr.lastIndexOf('/'); + if (slash > 0 && slash < cr.length() - 1) { + try { + return WorkspaceFileMetadata.ofFile(Long.parseLong(cr.substring(slash + 1).trim())); + } catch (NumberFormatException ignored) { + // fall through + } + } + } + long size = contentLength(response.headers().firstValue("Content-Length").orElse(null)); + if (size >= 0 && code == 200) { + return WorkspaceFileMetadata.ofFile(size); + } + // Last resort: full GET is too heavy — treat as missing size (force re-upload) + return WorkspaceFileMetadata.missing(); + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException("Failed to get metadata for " + absolutePath, e); + } + } + + private WorkspaceFileMetadata getFileMetadataDbfs(String path) throws HopException { + try { + String q = "/api/2.0/dbfs/get-status?path=" + URLEncoder.encode(path, StandardCharsets.UTF_8); + String body = get(q); + JSONObject json = parseObject(body); + if (Boolean.TRUE.equals(json.get("is_dir"))) { + return WorkspaceFileMetadata.missing(); + } + long size = requireLong(json, "file_size"); + return WorkspaceFileMetadata.ofFile(size); + } catch (HopException e) { + String msg = e.getMessage() == null ? "" : e.getMessage(); + if (msg.contains("404") + || msg.contains("RESOURCE_DOES_NOT_EXIST") + || msg.contains("File not found") + || msg.contains("does not exist")) { + return WorkspaceFileMetadata.missing(); + } + throw e; + } + } + + private Optional downloadTextFilesApi(String absolutePath) throws HopException { + try { + String encodedPath = encodeFilesApiPath(absolutePath); + String url = hostBase + FILES_API_PREFIX + encodedPath; + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(TIMEOUT) + .header("Authorization", "Bearer " + token) + .GET() + .build(); + HttpResponse response = + httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray()); + int code = response.statusCode(); + if (code == 404) { + return Optional.empty(); + } + if (code < 200 || code >= 300) { + String errBody = + response.body() == null ? "" : new String(response.body(), StandardCharsets.UTF_8); + throw new HopException( + "Databricks API HTTP " + + code + + " for GET " + + FILES_API_PREFIX + + absolutePath + + ": " + + sanitizeError(errBody)); + } + byte[] bytes = response.body() == null ? new byte[0] : response.body(); + return Optional.of(new String(bytes, StandardCharsets.UTF_8)); + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException("Failed to download " + absolutePath, e); + } + } + + private Optional downloadTextDbfs(String path) throws HopException { + try { + // Single-block read: small sidecar files only + String q = + "/api/2.0/dbfs/read?path=" + + URLEncoder.encode(path, StandardCharsets.UTF_8) + + "&offset=0&length=" + + (1024 * 1024); + String resp = get(q); + JSONObject json = parseObject(resp); + Object data = json.get("data"); + if (data == null) { + return Optional.of(""); + } + byte[] decoded = Base64.getDecoder().decode(data.toString()); + return Optional.of(new String(decoded, StandardCharsets.UTF_8)); + } catch (HopException e) { + String msg = e.getMessage() == null ? "" : e.getMessage(); + if (msg.contains("404") + || msg.contains("RESOURCE_DOES_NOT_EXIST") + || msg.contains("File not found") + || msg.contains("does not exist")) { + return Optional.empty(); + } + throw e; + } + } + + private static long contentLength(String header) { + if (StringUtils.isBlank(header)) { + return -1L; + } + try { + return Long.parseLong(header.trim()); + } catch (NumberFormatException e) { + return -1L; + } + } + + /** Legacy DBFS create / add-block / close for classic {@code dbfs:/} paths. */ + private void uploadViaDbfsApi(Path localFile, String path) throws HopException { + try { + JSONObject create = new JSONObject(); + create.put("path", path); + create.put("overwrite", true); + String createResp = postJson("/api/2.0/dbfs/create", create.toJSONString(), UPLOAD_TIMEOUT); + JSONObject createJson = parseObject(createResp); + long handle = requireLong(createJson, "handle"); + + try (InputStream in = Files.newInputStream(localFile)) { + byte[] buf = new byte[DBFS_BLOCK_BYTES]; + int n; + while ((n = in.read(buf)) >= 0) { + if (n == 0) { + continue; + } + byte[] chunk = n == buf.length ? buf : java.util.Arrays.copyOf(buf, n); + JSONObject add = new JSONObject(); + add.put("handle", handle); + add.put("data", Base64.getEncoder().encodeToString(chunk)); + postJson("/api/2.0/dbfs/add-block", add.toJSONString(), UPLOAD_TIMEOUT); + } + } + + JSONObject close = new JSONObject(); + close.put("handle", handle); + postJson("/api/2.0/dbfs/close", close.toJSONString(), UPLOAD_TIMEOUT); + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException("Failed to upload " + localFile + " to " + path + " via DBFS API", e); + } + } + + @Override + public void close() { + // HttpClient does not require close + } + + /** + * Normalize upload paths: strip optional {@code dbfs:} scheme, ensure a leading slash. {@code + * dbfs:/Volumes/…} becomes {@code /Volumes/…} so the Files API route is selected. + */ + static String normalizeDbfsPath(String dbfsPath) throws HopException { + if (StringUtils.isBlank(dbfsPath)) { + throw new HopException("Upload path is required"); + } + String p = dbfsPath.trim(); + if (p.startsWith("dbfs:")) { + p = p.substring("dbfs:".length()); + } + if (!p.startsWith("/")) { + p = "/" + p; + } + return p; + } + + /** + * Encode an absolute workspace path for the Files API URL path (keep {@code /} separators, encode + * each segment). + */ + static String encodeFilesApiPath(String absolutePath) { + String p = absolutePath.startsWith("/") ? absolutePath : "/" + absolutePath; + String[] parts = p.split("/", -1); + StringBuilder sb = new StringBuilder(); + for (String part : parts) { + if (part.isEmpty()) { + continue; + } + sb.append('/').append(URLEncoder.encode(part, StandardCharsets.UTF_8).replace("+", "%20")); + } + return sb.length() == 0 ? "/" : sb.toString(); + } + + private String get(String path) throws HopException { + return exchange("GET", path, null, TIMEOUT); + } + + private String postJson(String path, String jsonBody) throws HopException { + return postJson(path, jsonBody, TIMEOUT); + } + + private String postJson(String path, String jsonBody, Duration timeout) throws HopException { + return exchange("POST", path, jsonBody, timeout); + } + + private String exchange(String method, String path, String jsonBody, Duration timeout) + throws HopException { + try { + String url = hostBase + (path.startsWith("/") ? path : "/" + path); + HttpRequest.Builder builder = + HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(timeout) + .header("Authorization", "Bearer " + token) + .header("Content-Type", "application/json"); + if ("GET".equalsIgnoreCase(method)) { + builder.GET(); + } else { + builder.method( + method, + HttpRequest.BodyPublishers.ofString( + jsonBody == null ? "" : jsonBody, StandardCharsets.UTF_8)); + } + HttpResponse response = + httpClient.send( + builder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + int code = response.statusCode(); + String body = response.body() == null ? "" : response.body(); + if (code < 200 || code >= 300) { + throw new HopException( + "Databricks API HTTP " + + code + + " for " + + method + + " " + + path + + ": " + + sanitizeError(body)); + } + return body; + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException("Databricks API call failed: " + method + " " + path, e); + } + } + + private JSONObject parseObject(String body) throws HopException { + try { + Object parsed = parser.parse(body); + if (!(parsed instanceof JSONObject)) { + throw new HopException("Expected JSON object from Databricks API"); + } + return (JSONObject) parsed; + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException("Unable to parse Databricks API response", e); + } + } + + private static long requireLong(JSONObject json, String key) throws HopException { + Object v = json.get(key); + if (v instanceof Number number) { + return number.longValue(); + } + if (v != null) { + try { + return Long.parseLong(v.toString()); + } catch (NumberFormatException ignored) { + // fall through + } + } + throw new HopException("Databricks API response missing '" + key + "'"); + } + + /** Strip likely secrets from error payloads before logging. */ + static String sanitizeError(String body) { + if (body == null) { + return ""; + } + String trimmed = body.length() > 500 ? body.substring(0, 500) + "…" : body; + return trimmed.replaceAll("(?i)\\b(token|authorization|bearer)\\b\\s*[:=]?\\s*\\S+", "$1=***"); + } + + static String normalizeHost(String host) { + String h = host.trim(); + if (!h.startsWith("http://") && !h.startsWith("https://")) { + h = "https://" + h; + } + while (h.endsWith("/")) { + h = h.substring(0, h.length() - 1); + } + return h; + } + + static String normalizeApiBase(String apiBase) { + if (StringUtils.isBlank(apiBase)) { + return "/api/2.1"; + } + String b = apiBase.trim(); + if (!b.startsWith("/")) { + b = "/" + b; + } + while (b.endsWith("/") && b.length() > 1) { + b = b.substring(0, b.length() - 1); + } + return b; + } + + private static String resolve(IVariables variables, String value) { + if (value == null) { + return null; + } + return variables != null ? variables.resolve(value) : value; + } + + /** Build jar_params style list JSON helper for callers (not used by run-now map). */ + public static Map copyParams(Map in) { + return in == null ? Map.of() : new LinkedHashMap<>(in); + } +} diff --git a/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/client/WorkspaceFileMetadata.java b/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/client/WorkspaceFileMetadata.java new file mode 100644 index 00000000000..a246d452446 --- /dev/null +++ b/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/client/WorkspaceFileMetadata.java @@ -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. + */ + +package org.apache.hop.databricks.client; + +/** Result of probing a remote workspace path (UC Volume / Workspace / DBFS). */ +public record WorkspaceFileMetadata(boolean exists, long sizeBytes) { + + public static WorkspaceFileMetadata missing() { + return new WorkspaceFileMetadata(false, -1L); + } + + public static WorkspaceFileMetadata ofFile(long sizeBytes) { + return new WorkspaceFileMetadata(true, Math.max(0L, sizeBytes)); + } +} diff --git a/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/deploy/DatabricksJobSpecFactory.java b/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/deploy/DatabricksJobSpecFactory.java new file mode 100644 index 00000000000..248e7152346 --- /dev/null +++ b/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/deploy/DatabricksJobSpecFactory.java @@ -0,0 +1,603 @@ +/* + * 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.hop.databricks.deploy; + +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.Const; +import org.apache.hop.core.exception.HopException; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; + +/** Builds Jobs API JSON for Hop Native Spark (MainSpark JAR task) on Databricks. */ +public final class DatabricksJobSpecFactory { + + /** Driver entry on the native Spark fat jar (see engines/spark MainSpark). */ + public static final String MAIN_CLASS = "org.apache.hop.spark.run.MainSpark"; + + public static final String DEFAULT_TASK_KEY = "hop_native_spark"; + + /** + * Sentinel: emit a Jobs API {@code new_cluster} object (classic ephemeral job cluster) instead of + * {@code existing_cluster_id}. + */ + public static final String NEW_CLUSTER_TOKEN = "new_cluster"; + + /** + * Sentinel: serverless workspace compute — job-level {@code environments} + task {@code + * environment_key} (no cluster fields). + */ + public static final String SERVERLESS_TOKEN = "serverless"; + + public static final String DEFAULT_SPARK_VERSION = "18.2.x-scala2.13"; + + /** AWS default: local NVMe (i3) avoids required EBS on m5.* job clusters. */ + public static final String DEFAULT_NODE_TYPE_ID = "i3.xlarge"; + + public static final int DEFAULT_NUM_WORKERS = 1; + public static final String DEFAULT_ENVIRONMENT_KEY = "default"; + + /** Serverless environment client version (e.g. {@code 4} for Spark 4.x runtime). */ + public static final String DEFAULT_ENVIRONMENT_CLIENT = "4"; + + public enum ComputeMode { + EXISTING_CLUSTER, + NEW_CLUSTER, + SERVERLESS + } + + /** + * Resolved compute target for job create/reset. + * + * @param mode existing cluster, classic job cluster, or serverless environment + * @param existingClusterId set when {@link ComputeMode#EXISTING_CLUSTER} + * @param newCluster set when {@link ComputeMode#NEW_CLUSTER} + * @param environmentKey set when {@link ComputeMode#SERVERLESS} + * @param environmentClient set when {@link ComputeMode#SERVERLESS} (spec.client) + */ + public record ClusterTarget( + ComputeMode mode, + String existingClusterId, + JSONObject newCluster, + String environmentKey, + String environmentClient) {} + + private DatabricksJobSpecFactory() {} + + /** + * Create-job body: name + single spark_jar_task with jar library. + * + * @param jobName Databricks job display name + * @param clusterTarget resolved compute target (existing cluster, new cluster, or serverless) + * @param jarDbfsPath workspace or volume path of the fat jar + * @param launch MainSpark parameters (pipeline / metadata / package / env) + */ + public static String buildCreateJobJson( + String jobName, ClusterTarget clusterTarget, String jarDbfsPath, MainSparkLaunchSpec launch) + throws HopException { + JSONObject root = new JSONObject(); + root.put("name", require(jobName, "job name")); + String jarUri = toDbfsUri(jarDbfsPath); + applyEnvironments(root, clusterTarget, jarUri); + JSONArray tasks = new JSONArray(); + tasks.add(buildJarTask(clusterTarget, jarUri, launch)); + root.put("tasks", tasks); + return root.toJSONString(); + } + + /** + * Create-job body: name + single spark_jar_task with jar library. + * + * @param jobName Databricks job display name + * @param clusterTarget resolved compute target (existing cluster, new cluster, or serverless) + * @param jarDbfsPath workspace or volume path of the fat jar + * @param pipelineDbfsPath path of the uploaded pipeline file + * @param metadataDbfsPath path of the uploaded metadata JSON + * @param runConfigName optional pipeline run configuration name + */ + public static String buildCreateJobJson( + String jobName, + ClusterTarget clusterTarget, + String jarDbfsPath, + String pipelineDbfsPath, + String metadataDbfsPath, + String runConfigName) + throws HopException { + return buildCreateJobJson( + jobName, + clusterTarget, + jarDbfsPath, + MainSparkLaunchSpec.positional(pipelineDbfsPath, metadataDbfsPath, runConfigName)); + } + + /** + * Create-job with cluster field + optional new_cluster object (legacy callers). Prefer {@link + * #buildCreateJobJson(String, ClusterTarget, String, String, String, String)}. + */ + public static String buildCreateJobJson( + String jobName, + String clusterField, + JSONObject newCluster, + String jarDbfsPath, + String pipelineDbfsPath, + String metadataDbfsPath, + String runConfigName) + throws HopException { + ClusterTarget target = clusterTargetFromFieldAndObject(clusterField, newCluster); + return buildCreateJobJson( + jobName, target, jarDbfsPath, pipelineDbfsPath, metadataDbfsPath, runConfigName); + } + + /** Backward-compatible create: existing cluster only. */ + public static String buildCreateJobJson( + String jobName, + String existingClusterId, + String jarDbfsPath, + String pipelineDbfsPath, + String metadataDbfsPath, + String runConfigName) + throws HopException { + return buildCreateJobJson( + jobName, + existingClusterId, + null, + jarDbfsPath, + pipelineDbfsPath, + metadataDbfsPath, + runConfigName); + } + + /** jobs/reset body: job_id + new_settings. */ + public static String buildResetJobJson( + long jobId, + String jobName, + ClusterTarget clusterTarget, + String jarDbfsPath, + MainSparkLaunchSpec launch) + throws HopException { + JSONObject root = new JSONObject(); + root.put("job_id", jobId); + JSONObject settings = new JSONObject(); + settings.put("name", require(jobName, "job name")); + String jarUri = toDbfsUri(jarDbfsPath); + applyEnvironments(settings, clusterTarget, jarUri); + JSONArray tasks = new JSONArray(); + tasks.add(buildJarTask(clusterTarget, jarUri, launch)); + settings.put("tasks", tasks); + root.put("new_settings", settings); + return root.toJSONString(); + } + + /** jobs/reset body: job_id + new_settings. */ + public static String buildResetJobJson( + long jobId, + String jobName, + ClusterTarget clusterTarget, + String jarDbfsPath, + String pipelineDbfsPath, + String metadataDbfsPath, + String runConfigName) + throws HopException { + return buildResetJobJson( + jobId, + jobName, + clusterTarget, + jarDbfsPath, + MainSparkLaunchSpec.positional(pipelineDbfsPath, metadataDbfsPath, runConfigName)); + } + + public static String buildResetJobJson( + long jobId, + String jobName, + String clusterField, + JSONObject newCluster, + String jarDbfsPath, + String pipelineDbfsPath, + String metadataDbfsPath, + String runConfigName) + throws HopException { + ClusterTarget target = clusterTargetFromFieldAndObject(clusterField, newCluster); + return buildResetJobJson( + jobId, jobName, target, jarDbfsPath, pipelineDbfsPath, metadataDbfsPath, runConfigName); + } + + /** Backward-compatible reset: existing cluster only. */ + public static String buildResetJobJson( + long jobId, + String jobName, + String existingClusterId, + String jarDbfsPath, + String pipelineDbfsPath, + String metadataDbfsPath, + String runConfigName) + throws HopException { + return buildResetJobJson( + jobId, + jobName, + existingClusterId, + null, + jarDbfsPath, + pipelineDbfsPath, + metadataDbfsPath, + runConfigName); + } + + /** One-time runs/submit body with the same JAR task. */ + public static String buildSubmitRunJson( + String runName, ClusterTarget clusterTarget, String jarDbfsPath, MainSparkLaunchSpec launch) + throws HopException { + JSONObject root = new JSONObject(); + if (StringUtils.isNotBlank(runName)) { + root.put("run_name", runName); + } + String jarUri = toDbfsUri(jarDbfsPath); + applyEnvironments(root, clusterTarget, jarUri); + JSONArray tasks = new JSONArray(); + tasks.add(buildJarTask(clusterTarget, jarUri, launch)); + root.put("tasks", tasks); + return root.toJSONString(); + } + + /** One-time runs/submit body with the same JAR task. */ + public static String buildSubmitRunJson( + String runName, + ClusterTarget clusterTarget, + String jarDbfsPath, + String pipelineDbfsPath, + String metadataDbfsPath, + String runConfigName) + throws HopException { + return buildSubmitRunJson( + runName, + clusterTarget, + jarDbfsPath, + MainSparkLaunchSpec.positional(pipelineDbfsPath, metadataDbfsPath, runConfigName)); + } + + public static String buildSubmitRunJson( + String runName, + String clusterField, + JSONObject newCluster, + String jarDbfsPath, + String pipelineDbfsPath, + String metadataDbfsPath, + String runConfigName) + throws HopException { + ClusterTarget target = clusterTargetFromFieldAndObject(clusterField, newCluster); + return buildSubmitRunJson( + runName, target, jarDbfsPath, pipelineDbfsPath, metadataDbfsPath, runConfigName); + } + + public static boolean isNewClusterToken(String clusterField) { + return StringUtils.isNotBlank(clusterField) + && NEW_CLUSTER_TOKEN.equalsIgnoreCase(clusterField.trim()); + } + + public static boolean isServerlessToken(String clusterField) { + return StringUtils.isNotBlank(clusterField) + && SERVERLESS_TOKEN.equalsIgnoreCase(clusterField.trim()); + } + + /** + * Build the Jobs API {@code new_cluster} object from structured fields and/or a full JSON + * override. + */ + @SuppressWarnings("unchecked") + public static JSONObject buildNewClusterObject( + String sparkVersion, String nodeTypeId, String numWorkers, String newClusterJsonOptional) + throws HopException { + if (StringUtils.isNotBlank(newClusterJsonOptional)) { + try { + Object parsed = new JSONParser().parse(newClusterJsonOptional.trim()); + if (!(parsed instanceof JSONObject jsonObject)) { + throw new HopException("new_cluster JSON must be a JSON object"); + } + return jsonObject; + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException("Unable to parse new_cluster JSON", e); + } + } + + String version = + StringUtils.isNotBlank(sparkVersion) ? sparkVersion.trim() : DEFAULT_SPARK_VERSION; + String node = StringUtils.isNotBlank(nodeTypeId) ? nodeTypeId.trim() : DEFAULT_NODE_TYPE_ID; + int workers = Const.toInt(numWorkers, DEFAULT_NUM_WORKERS); + if (workers < 0) { + throw new HopException("new_cluster num_workers must be >= 0"); + } + + JSONObject cluster = new JSONObject(); + cluster.put("spark_version", version); + cluster.put("node_type_id", node); + cluster.put("num_workers", workers); + return cluster; + } + + /** + * Resolve cluster field into existing id, classic {@code new_cluster}, or serverless environment. + * + * @param environmentKey serverless environment_key (default {@code default}) + * @param environmentClient serverless {@code spec.client} (default {@code 4}) + */ + public static ClusterTarget resolveClusterTarget( + String clusterField, + String sparkVersion, + String nodeTypeId, + String numWorkers, + String newClusterJsonOptional, + String environmentKey, + String environmentClient) + throws HopException { + if (StringUtils.isBlank(clusterField)) { + throw new HopException( + "Missing cluster field: existing cluster id, new_cluster, or serverless"); + } + String field = clusterField.trim(); + if (isServerlessToken(field)) { + String envKey = + StringUtils.isNotBlank(environmentKey) ? environmentKey.trim() : DEFAULT_ENVIRONMENT_KEY; + String client = + StringUtils.isNotBlank(environmentClient) + ? environmentClient.trim() + : DEFAULT_ENVIRONMENT_CLIENT; + return new ClusterTarget(ComputeMode.SERVERLESS, null, null, envKey, client); + } + if (isNewClusterToken(field)) { + return new ClusterTarget( + ComputeMode.NEW_CLUSTER, + null, + buildNewClusterObject(sparkVersion, nodeTypeId, numWorkers, newClusterJsonOptional), + null, + null); + } + return new ClusterTarget(ComputeMode.EXISTING_CLUSTER, field, null, null, null); + } + + /** + * @deprecated use {@link #resolveClusterTarget(String, String, String, String, String, String, + * String)} + */ + @Deprecated(since = "2.19") + public static ClusterTarget resolveClusterTarget( + String clusterField, + String sparkVersion, + String nodeTypeId, + String numWorkers, + String newClusterJsonOptional) + throws HopException { + return resolveClusterTarget( + clusterField, + sparkVersion, + nodeTypeId, + numWorkers, + newClusterJsonOptional, + DEFAULT_ENVIRONMENT_KEY, + DEFAULT_ENVIRONMENT_CLIENT); + } + + private static ClusterTarget clusterTargetFromFieldAndObject( + String clusterField, JSONObject newCluster) throws HopException { + if (StringUtils.isBlank(clusterField)) { + throw new HopException( + "Missing cluster field: existing cluster id, new_cluster, or serverless"); + } + String field = clusterField.trim(); + if (isServerlessToken(field)) { + return new ClusterTarget( + ComputeMode.SERVERLESS, null, null, DEFAULT_ENVIRONMENT_KEY, DEFAULT_ENVIRONMENT_CLIENT); + } + if (isNewClusterToken(field)) { + if (newCluster == null || newCluster.isEmpty()) { + throw new HopException( + "new_cluster object is required when cluster field is '" + NEW_CLUSTER_TOKEN + "'"); + } + return new ClusterTarget(ComputeMode.NEW_CLUSTER, null, newCluster, null, null); + } + return new ClusterTarget(ComputeMode.EXISTING_CLUSTER, field, null, null, null); + } + + /** + * Serverless jobs declare libraries on the environment ({@code spec.dependencies}), not on the + * task. Classic modes leave environments unset. + */ + @SuppressWarnings("unchecked") + private static void applyEnvironments( + JSONObject jobOrSettings, ClusterTarget clusterTarget, String jarUri) throws HopException { + if (clusterTarget.mode() != ComputeMode.SERVERLESS) { + return; + } + JSONArray environments = new JSONArray(); + JSONObject env = new JSONObject(); + env.put("environment_key", require(clusterTarget.environmentKey(), "environment_key")); + JSONObject spec = new JSONObject(); + spec.put("client", require(clusterTarget.environmentClient(), "environment client")); + // Serverless JAR tasks require java_dependencies on the environment (not task libraries[]) + JSONArray javaDependencies = new JSONArray(); + javaDependencies.add(require(jarUri, "jar path")); + spec.put("java_dependencies", javaDependencies); + env.put("spec", spec); + environments.add(env); + jobOrSettings.put("environments", environments); + } + + static JSONObject buildJarTask( + ClusterTarget clusterTarget, + String jarUri, + String pipelineDbfsPath, + String metadataDbfsPath, + String runConfigName) + throws HopException { + return buildJarTask( + clusterTarget, + jarUri, + MainSparkLaunchSpec.positional(pipelineDbfsPath, metadataDbfsPath, runConfigName)); + } + + @SuppressWarnings("unchecked") + static JSONObject buildJarTask( + ClusterTarget clusterTarget, String jarUri, MainSparkLaunchSpec launch) throws HopException { + JSONObject task = new JSONObject(); + task.put("task_key", DEFAULT_TASK_KEY); + + switch (clusterTarget.mode()) { + case SERVERLESS -> + task.put("environment_key", require(clusterTarget.environmentKey(), "environment_key")); + case NEW_CLUSTER -> { + if (clusterTarget.newCluster() == null || clusterTarget.newCluster().isEmpty()) { + throw new HopException( + "new_cluster object is required when cluster field is '" + NEW_CLUSTER_TOKEN + "'"); + } + task.put("new_cluster", clusterTarget.newCluster()); + } + case EXISTING_CLUSTER -> + task.put( + "existing_cluster_id", + require(clusterTarget.existingClusterId(), "existing cluster id")); + default -> throw new HopException("Unsupported compute mode: " + clusterTarget.mode()); + } + + JSONObject jarTask = new JSONObject(); + jarTask.put("main_class_name", MAIN_CLASS); + jarTask.put("parameters", buildMainSparkParameters(launch)); + task.put("spark_jar_task", jarTask); + + if (clusterTarget.mode() != ComputeMode.SERVERLESS) { + JSONArray libraries = new JSONArray(); + JSONObject lib = new JSONObject(); + lib.put( + "jar", jarUri.startsWith("/") || jarUri.startsWith("dbfs:") ? jarUri : toDbfsUri(jarUri)); + libraries.add(lib); + task.put("libraries", libraries); + } + return task; + } + + /** + * Build {@code spark_jar_task.parameters} for MainSpark. Uses named {@code --Hop*} args when a + * project package or env config is present; otherwise classic positional pipeline/metadata/rc. + */ + static JSONArray buildMainSparkParameters(MainSparkLaunchSpec launch) throws HopException { + if (launch == null) { + throw new HopException("MainSpark launch specification is required"); + } + String runConfig = require(launch.runConfigName(), "run configuration name"); + JSONArray params = new JSONArray(); + if (!launch.useNamedParameters()) { + params.add(parameterPath(launch.pipelinePath())); + params.add(parameterPath(require(launch.metadataPath(), "metadata path"))); + params.add(runConfig); + return params; + } + + if (launch.hasProjectPackage()) { + params.add("--HopProjectPackage=" + parameterPath(launch.projectPackagePath())); + } + params.add("--HopPipelinePath=" + parameterPath(launch.pipelinePath())); + if (StringUtils.isNotBlank(launch.metadataPath())) { + params.add("--HopMetadataPath=" + parameterPath(launch.metadataPath())); + } else if (!launch.hasProjectPackage()) { + throw new HopException( + "Metadata path is required unless a Spark project package is uploaded"); + } + params.add("--HopRunConfigurationName=" + runConfig); + if (launch.hasConfigFile()) { + params.add("--HopConfigFile=" + parameterPath(launch.configFilePath())); + } + return params; + } + + /** + * Paths that are package-relative (no leading slash / scheme) stay as-is; workspace paths go + * through {@link #toDbfsUri(String)}. + */ + static String parameterPath(String path) throws HopException { + String p = require(path, "path"); + // Relative pipeline path inside a project package (e.g. pipelines/hello.hpl) + if (!p.startsWith("/") + && !p.startsWith("dbfs:") + && !p.contains("://") + && !p.matches("^[A-Za-z]:[\\\\/].*")) { + return p; + } + return toDbfsUri(p); + } + + /** + * @deprecated internal bridge for older call sites that pass field + object + */ + @Deprecated(since = "2.19") + static JSONObject buildJarTask( + String clusterField, + JSONObject newCluster, + String jarDbfsPath, + String pipelineDbfsPath, + String metadataDbfsPath, + String runConfigName) + throws HopException { + return buildJarTask( + clusterTargetFromFieldAndObject(clusterField, newCluster), + toDbfsUri(jarDbfsPath), + MainSparkLaunchSpec.positional(pipelineDbfsPath, metadataDbfsPath, runConfigName)); + } + + /** + * Normalize library / MainSpark parameter paths. + * + *

    + *
  • UC Volumes and Workspace files stay as absolute {@code /Volumes/…} or {@code + * /Workspace/…} (no {@code dbfs:} scheme). + *
  • Classic DBFS paths get a {@code dbfs:} scheme when missing. + *
+ */ + public static String toDbfsUri(String path) throws HopException { + String p = require(path, "workspace path"); + if (p.startsWith("dbfs:")) { + String stripped = p.substring("dbfs:".length()); + if (!stripped.startsWith("/")) { + stripped = "/" + stripped; + } + if (isVolumeOrWorkspacePath(stripped)) { + return stripped; + } + return "dbfs:" + stripped; + } + if (!p.startsWith("/")) { + p = "/" + p; + } + if (isVolumeOrWorkspacePath(p)) { + return p; + } + return "dbfs:" + p; + } + + static boolean isVolumeOrWorkspacePath(String absolutePath) { + return absolutePath.startsWith("/Volumes/") + || absolutePath.equals("/Volumes") + || absolutePath.startsWith("/Workspace/") + || absolutePath.equals("/Workspace"); + } + + private static String require(String value, String label) throws HopException { + if (StringUtils.isBlank(value)) { + throw new HopException("Missing " + label); + } + return value.trim(); + } +} diff --git a/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/deploy/HopSparkDeployHelper.java b/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/deploy/HopSparkDeployHelper.java new file mode 100644 index 00000000000..7efffc0adc7 --- /dev/null +++ b/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/deploy/HopSparkDeployHelper.java @@ -0,0 +1,665 @@ +/* + * 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.hop.databricks.deploy; + +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.util.HexFormat; +import java.util.Optional; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.core.metadata.SerializableMetadataProvider; +import org.apache.hop.core.plugins.IPlugin; +import org.apache.hop.core.plugins.PluginRegistry; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.core.vfs.HopVfs; +import org.apache.hop.databricks.client.DatabricksJobsClient; +import org.apache.hop.databricks.client.WorkspaceFileMetadata; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.engine.PipelineEnginePluginType; + +/** + * Uploads fat jar, pipeline, exported metadata, optional Spark project package, and optional env + * config for a MainSpark job on Databricks. Fat jar upload is skipped when remote size and SHA-256 + * sidecar match the local file. + */ +public final class HopSparkDeployHelper { + + /** + * Unversioned "latest" name (optional human convenience). Prefer {@link + * #fatJarRemoteName(String)} so each content change gets a new library URI — Databricks existing + * clusters often keep the previous JAR on the classpath when only the bytes at a fixed path are + * overwritten. + */ + public static final String FAT_JAR_REMOTE_NAME = "hop-native.jar"; + + /** Prefix for content-addressed fat jars: {@code hop-native-.jar}. */ + public static final String FAT_JAR_REMOTE_PREFIX = "hop-native-"; + + /** Hex chars of SHA-256 used in {@link #fatJarRemoteName(String)}. */ + public static final int FAT_JAR_SHA_PREFIX_LENGTH = 12; + + /** + * @deprecated fixed name; use {@link #pipelineRemoteName(String)} for concurrent-safe deploys + */ + public static final String PIPELINE_REMOTE_NAME = "pipeline.hpl"; + + /** + * @deprecated fixed name; use {@link #metadataRemoteName(String)} + */ + public static final String METADATA_REMOTE_NAME = "metadata.json"; + + /** Prefix for pipeline-scoped package zips: {@code hop-spark-package-{stem}.zip}. */ + public static final String PROJECT_PACKAGE_REMOTE_PREFIX = "hop-spark-package"; + + /** + * @deprecated fixed name; use {@link #projectPackageRemoteName(String)} + */ + public static final String PROJECT_PACKAGE_REMOTE_NAME = "hop-spark-package.zip"; + + /** + * @deprecated fixed name; use {@link #envConfigRemoteName(String)} + */ + public static final String ENV_CONFIG_REMOTE_NAME = "env-config.json"; + + public static final String SHA256_SIDECAR_SUFFIX = ".sha256"; + private static final int MAX_STEM_LENGTH = 80; + + /** + * Content-addressed remote fat jar filename for a local SHA-256 hex digest. + * + *

Example: {@code hop-native-2f80e51734a5.jar} for sha {@code 2f80e51734a5…}. + */ + public static String fatJarRemoteName(String sha256Hex) { + String sha = normalizeSha256(sha256Hex); + if (sha.length() < FAT_JAR_SHA_PREFIX_LENGTH) { + throw new IllegalArgumentException( + "SHA-256 hex too short for fat jar remote name (need >= " + + FAT_JAR_SHA_PREFIX_LENGTH + + "): " + + sha256Hex); + } + return FAT_JAR_REMOTE_PREFIX + sha.substring(0, FAT_JAR_SHA_PREFIX_LENGTH) + ".jar"; + } + + /** Plugin id of the native Spark pipeline engine (engines/spark). */ + public static final String SPARK_ENGINE_PLUGIN_ID = "SparkPipelineEngine"; + + private static final String SPARK_PROJECT_PACKAGE_CLASS = + "org.apache.hop.spark.pkg.SparkProjectPackage"; + + /** + * @param jarDbfs fat jar remote path + * @param pipelineDbfs uploaded pipeline path, or null when package-only relative path is used + * @param metadataDbfs metadata JSON path, or null when package embeds metadata + * @param projectPackageDbfs project package zip, or null + * @param envConfigDbfs environment config JSON, or null + * @param runConfigName Native Spark run configuration name + * @param launch ready-to-use MainSpark parameters for the Jobs API + */ + public record DeployedArtifacts( + String jarDbfs, + String pipelineDbfs, + String metadataDbfs, + String projectPackageDbfs, + String envConfigDbfs, + String runConfigName, + MainSparkLaunchSpec launch) { + + /** Backward-compatible view of the classic three paths. */ + public DeployedArtifacts { + if (launch == null) { + throw new IllegalArgumentException("launch is required"); + } + } + } + + /** + * Deploy options for optional project package and environment file. + * + * @param uploadProjectPackage when true, export or upload a Spark project package zip + * @param projectHome project home for export (default {@code PROJECT_HOME} variable) + * @param projectPackageFile optional existing zip; when blank and uploadProjectPackage, export + * @param environmentConfigFile optional local/VFS env JSON for MainSpark {@code --HopConfigFile} + */ + public record DeployOptions( + boolean uploadProjectPackage, + String projectHome, + String projectPackageFile, + String environmentConfigFile) { + + public static DeployOptions none() { + return new DeployOptions(false, null, null, null); + } + } + + private HopSparkDeployHelper() {} + + /** + * Export metadata, copy pipeline if needed, upload classic three artifacts under {@code + * dbfsBaseDir}. + */ + public static DeployedArtifacts deploy( + DatabricksJobsClient client, + IHopMetadataProvider metadataProvider, + IVariables variables, + ILogChannel log, + String localFatJar, + String localOrVfsPipeline, + String runConfigName, + String dbfsBaseDir) + throws HopException { + return deploy( + client, + metadataProvider, + variables, + log, + localFatJar, + localOrVfsPipeline, + runConfigName, + dbfsBaseDir, + DeployOptions.none()); + } + + /** + * Upload fat jar, pipeline/metadata and optional project package + environment config under + * {@code dbfsBaseDir}. + */ + public static DeployedArtifacts deploy( + DatabricksJobsClient client, + IHopMetadataProvider metadataProvider, + IVariables variables, + ILogChannel log, + String localFatJar, + String localOrVfsPipeline, + String runConfigName, + String dbfsBaseDir, + DeployOptions options) + throws HopException { + if (options == null) { + options = DeployOptions.none(); + } + String jar = resolve(variables, localFatJar); + String pipeline = resolve(variables, localOrVfsPipeline); + String runConfig = resolve(variables, runConfigName); + String base = normalizeBase(resolve(variables, dbfsBaseDir)); + + if (StringUtils.isBlank(jar)) { + throw new HopException("Fat jar path is required for deploy"); + } + if (StringUtils.isBlank(pipeline)) { + throw new HopException("Pipeline filename is required for deploy"); + } + if (StringUtils.isBlank(runConfig)) { + throw new HopException("Pipeline run configuration name is required for deploy"); + } + + Path jarPath = resolveLocalFile(jar, "Fat jar"); + + Path pipelineLocal = materializeToTemp(pipeline, "hop-dbx-pipeline-", ".hpl", "pipeline"); + + // Per-pipeline remote names so concurrent deploys from the same project do not clobber + // each other's package / pipeline / metadata / env files on the Volume. + String stem = remoteArtifactStem(pipeline); + // Content-addressed fat jar path: fixed hop-native.jar overwrites do not refresh libraries on + // a running Dedicated cluster (job still loads old MainSpark classes). A new URI does. + String localJarSha = sha256Hex(jarPath); + String jarDbfs = base + "/" + fatJarRemoteName(localJarSha); + String pipelineDbfs = base + "/" + pipelineRemoteName(stem); + String metadataDbfs = base + "/" + metadataRemoteName(stem); + String packageDbfs = null; + String envDbfs = null; + + if (log != null && log.isBasic()) { + log.logBasic( + "Fat jar library URI (content-addressed, sha256=" + + localJarSha + + "): " + + jarDbfs + + " — if a prior run still logged old package-distribution behavior after overwriting" + + " hop-native.jar, this new path forces Databricks to install a new library."); + } + uploadFatJarIfNeeded(client, log, jarPath, jarDbfs); + + // --- Project package (optional) --- + Path packageLocal = null; + boolean packageMode = options.uploadProjectPackage(); + String relativePipeline = null; + if (packageMode) { + String existingPkg = resolve(variables, options.projectPackageFile()); + if (StringUtils.isNotBlank(existingPkg)) { + packageLocal = resolveLocalFile(existingPkg, "Project package"); + } else { + String home = resolveProjectHome(variables, options.projectHome()); + try { + packageLocal = Files.createTempFile("hop-dbx-spark-pkg-", ".zip"); + packageLocal.toFile().deleteOnExit(); + } catch (Exception e) { + throw new HopException("Unable to create temp file for Spark project package", e); + } + if (log != null && log.isBasic()) { + log.logBasic("Exporting Spark project package from " + home); + } + exportSparkProjectPackage(home, packageLocal.toString(), metadataProvider, variables); + } + packageDbfs = base + "/" + projectPackageRemoteName(stem); + if (log != null && log.isBasic()) { + log.logBasic("Uploading project package to " + packageDbfs); + } + client.uploadToDbfs(packageLocal, packageDbfs); + + String homeForRel = resolveProjectHome(variables, options.projectHome()); + relativePipeline = relativePipelinePath(pipeline, homeForRel, variables); + } + + // --- Pipeline: always upload in simple mode; upload as fallback when not relative --- + boolean useRelativePipeline = packageMode && relativePipeline != null; + if (!useRelativePipeline) { + if (log != null && log.isBasic()) { + log.logBasic("Uploading pipeline to " + pipelineDbfs); + } + client.uploadToDbfs(pipelineLocal, pipelineDbfs); + } else if (log != null && log.isDetailed()) { + log.logDetailed( + "Using package-relative pipeline path '" + + relativePipeline + + "' (not uploading separate pipeline file)"); + } + + // --- Metadata: always in simple mode; package embeds metadata when package mode --- + String metadataRemote = null; + if (!packageMode) { + Path metadataLocal; + try { + String json = new SerializableMetadataProvider(metadataProvider).toJson(); + metadataLocal = Files.createTempFile("hop-dbx-metadata-", ".json"); + metadataLocal.toFile().deleteOnExit(); + Files.writeString(metadataLocal, json, StandardCharsets.UTF_8); + } catch (Exception e) { + throw new HopException("Unable to export Hop metadata to JSON", e); + } + if (log != null && log.isBasic()) { + log.logBasic("Uploading metadata to " + metadataDbfs); + } + client.uploadToDbfs(metadataLocal, metadataDbfs); + metadataRemote = metadataDbfs; + } + + // --- Environment config (optional) --- + String envLocalPath = resolve(variables, options.environmentConfigFile()); + if (StringUtils.isNotBlank(envLocalPath)) { + Path envLocal = + materializeToTemp(envLocalPath, "hop-dbx-env-", ".json", "environment config"); + envDbfs = base + "/" + envConfigRemoteName(stem); + if (log != null && log.isBasic()) { + log.logBasic("Uploading environment config to " + envDbfs); + } + client.uploadToDbfs(envLocal, envDbfs); + } + + String launchPipeline = useRelativePipeline ? relativePipeline : pipelineDbfs; + MainSparkLaunchSpec launch = + new MainSparkLaunchSpec(launchPipeline, metadataRemote, runConfig, packageDbfs, envDbfs); + + return new DeployedArtifacts( + jarDbfs, + useRelativePipeline ? null : pipelineDbfs, + metadataRemote, + packageDbfs, + envDbfs, + runConfig, + launch); + } + + /** + * Sanitize pipeline path to a Volume-safe artifact stem (basename without extension). + * + *

Example: {@code /path/hello-mapping-databricks.hpl} → {@code hello-mapping-databricks}. + */ + static String remoteArtifactStem(String pipelinePath) { + if (StringUtils.isBlank(pipelinePath)) { + return "pipeline"; + } + String p = pipelinePath.trim().replace('\\', '/'); + int slash = p.lastIndexOf('/'); + String base = slash >= 0 ? p.substring(slash + 1) : p; + int dot = base.lastIndexOf('.'); + if (dot > 0) { + base = base.substring(0, dot); + } + if (StringUtils.isBlank(base)) { + return "pipeline"; + } + StringBuilder sb = new StringBuilder(base.length()); + for (int i = 0; i < base.length(); i++) { + char c = base.charAt(i); + if ((c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9') + || c == '.' + || c == '_' + || c == '-') { + sb.append(c); + } else { + sb.append('-'); + } + } + String stem = sb.toString().replaceAll("-+", "-"); + while (stem.startsWith("-")) { + stem = stem.substring(1); + } + while (stem.endsWith("-")) { + stem = stem.substring(0, stem.length() - 1); + } + if (stem.isEmpty()) { + return "pipeline"; + } + if (stem.length() > MAX_STEM_LENGTH) { + stem = stem.substring(0, MAX_STEM_LENGTH); + } + return stem; + } + + static String projectPackageRemoteName(String stem) { + return PROJECT_PACKAGE_REMOTE_PREFIX + "-" + stem + ".zip"; + } + + static String pipelineRemoteName(String stem) { + return "pipeline-" + stem + ".hpl"; + } + + static String metadataRemoteName(String stem) { + return "metadata-" + stem + ".json"; + } + + static String envConfigRemoteName(String stem) { + return "env-config-" + stem + ".json"; + } + + /** Resolve project home: explicit field, else {@code PROJECT_HOME} variable. */ + static String resolveProjectHome(IVariables variables, String projectHomeField) + throws HopException { + String home = resolve(variables, projectHomeField); + if (StringUtils.isBlank(home) && variables != null) { + home = resolve(variables, variables.getVariable("PROJECT_HOME")); + } + if (StringUtils.isBlank(home)) { + throw new HopException( + "Project home is required to export a Spark project package. Set PROJECT_HOME or the" + + " Project home field on the Databricks Job Run action."); + } + return home; + } + + /** + * If {@code pipelinePath} is under {@code projectHome}, return a package-relative path using + * forward slashes; otherwise null (caller should upload pipeline.hpl). + */ + static String relativePipelinePath( + String pipelinePath, String projectHome, IVariables variables) { + if (StringUtils.isBlank(pipelinePath) || StringUtils.isBlank(projectHome)) { + return null; + } + try { + String pipe = resolve(variables, pipelinePath); + String home = resolve(variables, projectHome); + Path pipePath = Paths.get(pipe).toAbsolutePath().normalize(); + Path homePath = Paths.get(home).toAbsolutePath().normalize(); + if (!pipePath.startsWith(homePath)) { + // try VFS-resolved local + try { + pipePath = + Paths.get(HopVfs.getFileObject(pipe).getURL().toURI()).toAbsolutePath().normalize(); + } catch (Exception ignored) { + return null; + } + if (!pipePath.startsWith(homePath)) { + return null; + } + } + Path rel = homePath.relativize(pipePath); + return rel.toString().replace('\\', '/'); + } catch (Exception e) { + return null; + } + } + + /** + * Call {@code SparkProjectPackage.exportProject} via the Spark engine plugin classloader (no + * compile dependency on hop-engines-spark). + */ + static void exportSparkProjectPackage( + String projectHome, + String zipFilename, + IHopMetadataProvider metadataProvider, + IVariables variables) + throws HopException { + try { + PluginRegistry registry = PluginRegistry.getInstance(); + IPlugin plugin = + registry.findPluginWithId(PipelineEnginePluginType.class, SPARK_ENGINE_PLUGIN_ID); + if (plugin == null) { + throw new HopException( + "Native Spark engine plugin '" + + SPARK_ENGINE_PLUGIN_ID + + "' is not installed. Install plugins/engines/spark to export a project package," + + " or provide an existing package zip path."); + } + ClassLoader cl = registry.getClassLoader(plugin); + Class clazz = Class.forName(SPARK_PROJECT_PACKAGE_CLASS, true, cl); + Method export = + clazz.getMethod( + "exportProject", + String.class, + String.class, + IHopMetadataProvider.class, + IVariables.class); + export.invoke(null, projectHome, zipFilename, metadataProvider, variables); + } catch (HopException e) { + throw e; + } catch (ReflectiveOperationException e) { + Throwable cause = e.getCause() != null ? e.getCause() : e; + if (cause instanceof HopException he) { + throw he; + } + throw new HopException( + "Unable to export Spark project package via " + + SPARK_PROJECT_PACKAGE_CLASS + + ": " + + cause.getMessage(), + cause); + } catch (Exception e) { + throw new HopException("Unable to export Spark project package: " + e.getMessage(), e); + } + } + + static Path materializeToTemp(String pathOrVfs, String prefix, String suffix, String label) + throws HopException { + try { + if (Files.isRegularFile(Paths.get(pathOrVfs))) { + return Paths.get(pathOrVfs); + } + Path temp = Files.createTempFile(prefix, suffix); + temp.toFile().deleteOnExit(); + try (var in = HopVfs.getInputStream(pathOrVfs); + var out = Files.newOutputStream(temp)) { + in.transferTo(out); + } + return temp; + } catch (HopException e) { + throw e; + } catch (Exception e) { + throw new HopException("Unable to read " + label + ": " + pathOrVfs, e); + } + } + + static Path resolveLocalFile(String pathOrVfs, String label) throws HopException { + Path p = Paths.get(pathOrVfs); + if (Files.isRegularFile(p)) { + return p; + } + try { + p = Paths.get(HopVfs.getFileObject(pathOrVfs).getURL().toURI()); + } catch (Exception e) { + throw new HopException(label + " not found: " + pathOrVfs, e); + } + if (!Files.isRegularFile(p)) { + throw new HopException(label + " not found: " + pathOrVfs); + } + return p; + } + + /** + * Upload fat jar only when remote is missing, size differs, or SHA-256 sidecar does not match. + * Sidecar path is {@code remoteJarPath + ".sha256"} (hex digest only, UTF-8). + */ + static void uploadFatJarIfNeeded( + DatabricksJobsClient client, ILogChannel log, Path localJar, String remoteJarPath) + throws HopException { + long localSize; + try { + localSize = Files.size(localJar); + } catch (Exception e) { + throw new HopException("Unable to read local fat jar size: " + localJar, e); + } + String localSha = sha256Hex(localJar); + String sidecarPath = remoteJarPath + SHA256_SIDECAR_SUFFIX; + + if (remoteJarMatches(client, remoteJarPath, sidecarPath, localSize, localSha)) { + if (log != null && log.isBasic()) { + log.logBasic( + "Skipping fat jar upload (remote matches local size=" + + localSize + + " sha256=" + + localSha + + "): " + + remoteJarPath + + " — if Databricks still runs old package-distribution behavior, restart the" + + " target cluster or delete the remote jar + .sha256 sidecar and re-deploy."); + } + return; + } + + if (log != null && log.isBasic()) { + log.logBasic( + "Uploading fat jar to " + + remoteJarPath + + " (size=" + + localSize + + " sha256=" + + localSha + + ")"); + } + client.uploadToDbfs(localJar, remoteJarPath); + client.uploadText(sidecarPath, localSha + "\n"); + if (log != null && log.isBasic()) { + log.logBasic( + "Wrote fat jar checksum sidecar " + + sidecarPath + + " (sha256=" + + localSha + + "). On long-lived clusters, restart the cluster if the next job still logs an old" + + " Spark project package distribution build id."); + } + } + + static boolean remoteJarMatches( + DatabricksJobsClient client, + String remoteJarPath, + String sidecarPath, + long localSize, + String localSha) + throws HopException { + WorkspaceFileMetadata remote = client.getFileMetadata(remoteJarPath); + if (!remote.exists() || remote.sizeBytes() != localSize) { + return false; + } + Optional remoteSidecar = client.downloadTextIfExists(sidecarPath); + if (remoteSidecar.isEmpty()) { + return false; + } + String remoteSha = normalizeSha256(remoteSidecar.get()); + return localSha.equalsIgnoreCase(remoteSha); + } + + /** SHA-256 hex of file contents (lowercase). */ + static String sha256Hex(Path file) throws HopException { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + try (InputStream in = Files.newInputStream(file); + DigestInputStream din = new DigestInputStream(in, digest)) { + din.transferTo(OutputStream.nullOutputStream()); + } + return HexFormat.of().formatHex(digest.digest()); + } catch (Exception e) { + throw new HopException("Unable to compute SHA-256 of " + file, e); + } + } + + /** Accept "hex", "hex filename", or whitespace-padded sidecars. */ + static String normalizeSha256(String sidecarContent) { + if (sidecarContent == null) { + return ""; + } + String t = sidecarContent.trim(); + if (t.isEmpty()) { + return ""; + } + int space = t.indexOf(' '); + if (space > 0) { + t = t.substring(0, space).trim(); + } + int tab = t.indexOf('\t'); + if (tab > 0) { + t = t.substring(0, tab).trim(); + } + return t.toLowerCase(); + } + + static String normalizeBase(String dbfsBaseDir) throws HopException { + if (StringUtils.isBlank(dbfsBaseDir)) { + throw new HopException("DBFS base directory is required"); + } + String b = dbfsBaseDir.trim(); + if (b.startsWith("dbfs:")) { + b = b.substring("dbfs:".length()); + } + if (!b.startsWith("/")) { + b = "/" + b; + } + while (b.endsWith("/") && b.length() > 1) { + b = b.substring(0, b.length() - 1); + } + return b; + } + + private static String resolve(IVariables variables, String value) { + if (value == null) { + return null; + } + return variables != null ? variables.resolve(value) : value; + } +} diff --git a/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/deploy/MainSparkLaunchSpec.java b/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/deploy/MainSparkLaunchSpec.java new file mode 100644 index 00000000000..a97d56ad120 --- /dev/null +++ b/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/deploy/MainSparkLaunchSpec.java @@ -0,0 +1,71 @@ +/* + * 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.hop.databricks.deploy; + +import org.apache.commons.lang3.StringUtils; + +/** + * Paths and names passed as {@code spark_jar_task.parameters} to {@code + * org.apache.hop.spark.run.MainSpark}. + * + *

When {@link #projectPackagePath} or {@link #configFilePath} is set, the job uses named {@code + * --Hop*} arguments (required for project package). Otherwise three positional arguments match the + * classic deploy layout. + * + * @param pipelinePath entry pipeline path (Volume/DBFS URI, or package-relative path when using a + * project package) + * @param metadataPath metadata JSON path; may be null when a project package embeds metadata + * @param runConfigName Native Spark pipeline run configuration name + * @param projectPackagePath optional Spark project package zip on the workspace + * @param configFilePath optional environment / described-variables JSON for {@code --HopConfigFile} + */ +public record MainSparkLaunchSpec( + String pipelinePath, + String metadataPath, + String runConfigName, + String projectPackagePath, + String configFilePath) { + + public MainSparkLaunchSpec { + if (StringUtils.isBlank(pipelinePath)) { + throw new IllegalArgumentException("pipelinePath is required"); + } + if (StringUtils.isBlank(runConfigName)) { + throw new IllegalArgumentException("runConfigName is required"); + } + } + + /** Classic three-argument deploy (pipeline + metadata + run config). */ + public static MainSparkLaunchSpec positional( + String pipelinePath, String metadataPath, String runConfigName) { + return new MainSparkLaunchSpec(pipelinePath, metadataPath, runConfigName, null, null); + } + + /** True when named {@code --Hop*} parameters must be used. */ + public boolean useNamedParameters() { + return StringUtils.isNotBlank(projectPackagePath) || StringUtils.isNotBlank(configFilePath); + } + + public boolean hasProjectPackage() { + return StringUtils.isNotBlank(projectPackagePath); + } + + public boolean hasConfigFile() { + return StringUtils.isNotBlank(configFilePath); + } +} diff --git a/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/metadata/DatabricksConnection.java b/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/metadata/DatabricksConnection.java new file mode 100644 index 00000000000..5f9d235ff2c --- /dev/null +++ b/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/metadata/DatabricksConnection.java @@ -0,0 +1,91 @@ +/* + * 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.hop.databricks.metadata; + +import java.io.Serializable; +import lombok.Getter; +import lombok.Setter; +import org.apache.hop.core.gui.plugin.GuiElementType; +import org.apache.hop.core.gui.plugin.GuiPlugin; +import org.apache.hop.core.gui.plugin.GuiWidgetElement; +import org.apache.hop.metadata.api.HopMetadata; +import org.apache.hop.metadata.api.HopMetadataBase; +import org.apache.hop.metadata.api.HopMetadataCategory; +import org.apache.hop.metadata.api.HopMetadataProperty; +import org.apache.hop.metadata.api.IHopMetadata; + +/** + * Workspace connection for the Databricks REST / Jobs API (not JDBC SQL warehouse). Used by + * Databricks Job Run actions and related Jobs API clients. + */ +@Getter +@Setter +@GuiPlugin +@HopMetadata( + key = "DatabricksConnection", + name = "i18n::DatabricksConnection.Name", + description = "i18n::DatabricksConnection.Description", + image = "databricks-connection.svg", + category = HopMetadataCategory.CONNECTIONS, + documentationUrl = "/metadata-types/databricks-connection.html") +public class DatabricksConnection extends HopMetadataBase implements Serializable, IHopMetadata { + + private static final String PARENT = DatabricksConnectionEditor.GUI_WIDGETS_PARENT_ID; + + @GuiWidgetElement( + id = "10000-description", + parentId = PARENT, + type = GuiElementType.TEXT, + label = "i18n::DatabricksConnection.Description.Label", + toolTip = "i18n::DatabricksConnection.Description.Tooltip") + @HopMetadataProperty + private String description; + + @GuiWidgetElement( + id = "10010-host", + parentId = PARENT, + type = GuiElementType.TEXT, + label = "i18n::DatabricksConnection.Host.Label", + toolTip = "i18n::DatabricksConnection.Host.Tooltip") + @HopMetadataProperty + private String host; + + @GuiWidgetElement( + id = "10020-token", + parentId = PARENT, + type = GuiElementType.TEXT, + password = true, + label = "i18n::DatabricksConnection.Token.Label", + toolTip = "i18n::DatabricksConnection.Token.Tooltip") + @HopMetadataProperty(password = true) + private String token; + + /** Optional API base path; default empty means {@code /api/2.1}. */ + @GuiWidgetElement( + id = "10030-api-base", + parentId = PARENT, + type = GuiElementType.TEXT, + label = "i18n::DatabricksConnection.ApiBase.Label", + toolTip = "i18n::DatabricksConnection.ApiBase.Tooltip") + @HopMetadataProperty + private String apiBasePath; + + public DatabricksConnection() { + this.apiBasePath = "/api/2.1"; + } +} diff --git a/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/metadata/DatabricksConnectionEditor.java b/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/metadata/DatabricksConnectionEditor.java new file mode 100644 index 00000000000..c71499ac9f3 --- /dev/null +++ b/plugins/tech/databricks/src/main/java/org/apache/hop/databricks/metadata/DatabricksConnectionEditor.java @@ -0,0 +1,169 @@ +/* + * 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.hop.databricks.metadata; + +import org.apache.hop.core.Const; +import org.apache.hop.core.gui.plugin.GuiPlugin; +import org.apache.hop.databricks.client.DatabricksJobsClient; +import org.apache.hop.databricks.client.RestDatabricksJobsClient; +import org.apache.hop.i18n.BaseMessages; +import org.apache.hop.ui.core.PropsUi; +import org.apache.hop.ui.core.dialog.ErrorDialog; +import org.apache.hop.ui.core.dialog.MessageBox; +import org.apache.hop.ui.core.gui.GuiCompositeWidgets; +import org.apache.hop.ui.core.gui.GuiCompositeWidgetsAdapter; +import org.apache.hop.ui.core.metadata.MetadataEditor; +import org.apache.hop.ui.core.metadata.MetadataManager; +import org.apache.hop.ui.hopgui.HopGui; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +@GuiPlugin(description = "Editor for Databricks workspace connection metadata") +public class DatabricksConnectionEditor extends MetadataEditor { + + private static final Class PKG = DatabricksConnection.class; + + public static final String GUI_WIDGETS_PARENT_ID = "DatabricksConnectionEditor-GuiWidgetsParent"; + + private Text wName; + private Composite wWidgetsComposite; + private GuiCompositeWidgets guiCompositeWidgets; + + public DatabricksConnectionEditor( + HopGui hopGui, MetadataManager manager, DatabricksConnection metadata) { + super(hopGui, manager, metadata); + } + + @Override + public void createControl(Composite parent) { + PropsUi props = PropsUi.getInstance(); + int middle = props.getMiddlePct(); + int margin = PropsUi.getMargin() + 2; + + Label wIcon = new Label(parent, SWT.RIGHT); + wIcon.setImage(getImage()); + FormData fdlIcon = new FormData(); + fdlIcon.top = new FormAttachment(0, 0); + fdlIcon.right = new FormAttachment(100, 0); + wIcon.setLayoutData(fdlIcon); + PropsUi.setLook(wIcon); + + Label wlName = new Label(parent, SWT.RIGHT); + PropsUi.setLook(wlName); + wlName.setText(BaseMessages.getString(PKG, "DatabricksConnection.Name.Label")); + FormData fdlName = new FormData(); + fdlName.top = new FormAttachment(0, margin); + fdlName.left = new FormAttachment(0, 0); + fdlName.right = new FormAttachment(middle, -margin); + wlName.setLayoutData(fdlName); + wName = new Text(parent, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wName); + FormData fdName = new FormData(); + fdName.top = new FormAttachment(wlName, 0, SWT.CENTER); + fdName.left = new FormAttachment(middle, 0); + fdName.right = new FormAttachment(wIcon, -margin); + wName.setLayoutData(fdName); + Control lastControl = wName; + + wWidgetsComposite = new Composite(parent, SWT.NONE); + PropsUi.setLook(wWidgetsComposite); + wWidgetsComposite.setLayout(new FormLayout()); + FormData fdWidgetsComposite = new FormData(); + fdWidgetsComposite.top = new FormAttachment(lastControl, margin); + fdWidgetsComposite.left = new FormAttachment(0, 0); + fdWidgetsComposite.right = new FormAttachment(100, 0); + fdWidgetsComposite.bottom = new FormAttachment(100, -margin * 4); + wWidgetsComposite.setLayoutData(fdWidgetsComposite); + + guiCompositeWidgets = new GuiCompositeWidgets(manager.getVariables()); + guiCompositeWidgets.createCompositeWidgets( + metadata, null, wWidgetsComposite, GUI_WIDGETS_PARENT_ID, lastControl); + guiCompositeWidgets.setWidgetsListener( + new GuiCompositeWidgetsAdapter() { + @Override + public void widgetModified( + GuiCompositeWidgets compositeWidgets, Control changedWidget, String widgetId) { + setChanged(); + } + }); + + Button wTest = new Button(parent, SWT.PUSH); + PropsUi.setLook(wTest); + wTest.setText(BaseMessages.getString(PKG, "DatabricksConnection.Test.Label")); + FormData fdTest = new FormData(); + fdTest.bottom = new FormAttachment(100, 0); + fdTest.left = new FormAttachment(middle, 0); + wTest.setLayoutData(fdTest); + wTest.addListener(SWT.Selection, e -> testConnection()); + + setWidgetsContent(); + resetChanged(); + wName.addModifyListener(e -> setChanged()); + } + + private void testConnection() { + try { + getWidgetsContent(getMetadata()); + DatabricksConnection conn = getMetadata(); + try (DatabricksJobsClient client = + RestDatabricksJobsClient.create(conn, manager.getVariables())) { + String who = client.testConnection(); + MessageBox box = + new MessageBox(HopGui.getInstance().getShell(), SWT.OK | SWT.ICON_INFORMATION); + box.setText(BaseMessages.getString(PKG, "DatabricksConnection.Test.Success.Title")); + box.setMessage( + BaseMessages.getString(PKG, "DatabricksConnection.Test.Success.Message", who)); + box.open(); + } + } catch (Exception e) { + new ErrorDialog( + HopGui.getInstance().getShell(), + BaseMessages.getString(PKG, "DatabricksConnection.Test.Error.Title"), + BaseMessages.getString(PKG, "DatabricksConnection.Test.Error.Message"), + e); + } + } + + @Override + public void setWidgetsContent() { + DatabricksConnection meta = this.getMetadata(); + wName.setText(Const.NVL(meta.getName(), "")); + guiCompositeWidgets.setWidgetsContents(metadata, wWidgetsComposite, GUI_WIDGETS_PARENT_ID); + } + + @Override + public void getWidgetsContent(DatabricksConnection meta) { + meta.setName(wName.getText()); + guiCompositeWidgets.getWidgetsContents(metadata, GUI_WIDGETS_PARENT_ID); + } + + @Override + public boolean setFocus() { + if (wName == null || wName.isDisposed()) { + return false; + } + return wName.setFocus(); + } +} diff --git a/plugins/tech/databricks/src/main/java/org/apache/hop/workflow/actions/databricks/ActionDatabricksJobRun.java b/plugins/tech/databricks/src/main/java/org/apache/hop/workflow/actions/databricks/ActionDatabricksJobRun.java new file mode 100644 index 00000000000..26df204911e --- /dev/null +++ b/plugins/tech/databricks/src/main/java/org/apache/hop/workflow/actions/databricks/ActionDatabricksJobRun.java @@ -0,0 +1,569 @@ +/* + * 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.hop.workflow.actions.databricks; + +import java.util.List; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; +import org.apache.hop.core.Const; +import org.apache.hop.core.ICheckResult; +import org.apache.hop.core.Result; +import org.apache.hop.core.annotations.Action; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.file.IHasFilename; +import org.apache.hop.core.util.CurrentDirectoryResolver; +import org.apache.hop.core.util.Utils; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.databricks.client.DatabricksJobsClient; +import org.apache.hop.databricks.client.DatabricksRunLifeCycleState; +import org.apache.hop.databricks.client.DatabricksRunStatus; +import org.apache.hop.databricks.client.DatabricksRunWaiter; +import org.apache.hop.databricks.client.RestDatabricksJobsClient; +import org.apache.hop.databricks.deploy.DatabricksJobSpecFactory; +import org.apache.hop.databricks.deploy.HopSparkDeployHelper; +import org.apache.hop.databricks.metadata.DatabricksConnection; +import org.apache.hop.i18n.BaseMessages; +import org.apache.hop.metadata.api.HopMetadataProperty; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.pipeline.PipelineMeta; +import org.apache.hop.workflow.WorkflowMeta; +import org.apache.hop.workflow.action.ActionBase; +import org.apache.hop.workflow.action.IAction; +import org.apache.hop.workflow.action.validator.ActionValidatorUtils; +import org.apache.hop.workflow.action.validator.AndValidator; +import org.apache.hop.workflow.engine.IWorkflowEngine; + +/** + * Run or submit a Databricks job and optionally wait for completion. Sets result variables for Job + * ID, Run ID, and status. + */ +@Action( + id = "DATABRICKS_JOB_RUN", + name = "i18n::ActionDatabricksJobRun.Name", + description = "i18n::ActionDatabricksJobRun.Description", + image = "databricks-run.svg", + categoryDescription = "i18n:org.apache.hop.workflow:ActionCategory.Category.BigData", + keywords = "i18n::ActionDatabricksJobRun.keyword", + documentationUrl = "/workflow/actions/databricks-job-run.html") +@Getter +@Setter +public class ActionDatabricksJobRun extends ActionBase implements Cloneable, IAction { + + private static final Class PKG = ActionDatabricksJobRun.class; + + public static final String MODE_RUN_EXISTING = "RUN_EXISTING"; + public static final String MODE_SUBMIT_ONCE = "SUBMIT_ONCE"; + public static final String MODE_DEPLOY_AND_RUN = "DEPLOY_AND_RUN"; + + public static final String WAIT_WAIT = "WAIT"; + public static final String WAIT_FIRE_AND_FORGET = "FIRE_AND_FORGET"; + + @HopMetadataProperty(key = "connection") + private String connectionName; + + /** {@link #MODE_RUN_EXISTING}, {@link #MODE_SUBMIT_ONCE}, or {@link #MODE_DEPLOY_AND_RUN}. */ + @HopMetadataProperty(key = "run_mode") + private String runMode = MODE_RUN_EXISTING; + + @HopMetadataProperty(key = "job_id") + private String jobId; + + /** Raw Jobs API JSON for runs/submit (one-time). */ + @HopMetadataProperty(key = "submit_json") + private String submitRunJson; + + /** Local fat jar path for deploy mode. */ + @HopMetadataProperty(key = "fat_jar") + private String fatJarPath; + + /** Pipeline .hpl path for deploy mode. */ + @HopMetadataProperty(key = "pipeline_file") + private String pipelineFilename; + + /** Native Spark pipeline run configuration name (inside exported metadata). */ + @HopMetadataProperty(key = "run_configuration") + private String runConfigurationName; + + /** + * Upload root for deploy mode. Prefer a UC Volume path when DBFS root is disabled, e.g. {@code + * /Volumes/catalog/schema/volume/hop}. Classic: {@code dbfs:/FileStore/hop}. + */ + @HopMetadataProperty(key = "dbfs_base") + private String dbfsBasePath = "dbfs:/FileStore/hop"; + + /** + * When true, export/upload a Native Spark project package zip so nested Simple Mapping / Pipeline + * Executor paths under {@code PROJECT_HOME} resolve on the cluster. + */ + @HopMetadataProperty(key = "upload_project_package") + private boolean uploadProjectPackage; + + /** + * Project home used when exporting a package (default: {@code ${PROJECT_HOME}} / active project). + */ + @HopMetadataProperty(key = "project_home") + private String projectHome; + + /** + * Optional existing Spark project package zip. When blank and {@link #uploadProjectPackage} is + * true, the package is exported from {@link #projectHome}. + */ + @HopMetadataProperty(key = "project_package_file") + private String projectPackageFile; + + /** + * Optional local environment / described-variables JSON uploaded and passed to MainSpark as + * {@code --HopConfigFile}. + */ + @HopMetadataProperty(key = "environment_config_file") + private String environmentConfigFile; + + /** + * Existing all-purpose cluster id, or sentinel {@code new_cluster} (classic job cluster), or + * {@code serverless} (job environments + task environment_key; no cluster fields). + */ + @HopMetadataProperty(key = "cluster_id") + private String existingClusterId; + + /** DBR spark_version for classic job clusters, e.g. {@code 18.2.x-scala2.13}. */ + @HopMetadataProperty(key = "new_cluster_spark_version") + private String newClusterSparkVersion = DatabricksJobSpecFactory.DEFAULT_SPARK_VERSION; + + /** + * Cloud node type for classic job clusters, e.g. {@code i3.xlarge} (AWS) or {@code + * Standard_DS3_v2}. + */ + @HopMetadataProperty(key = "new_cluster_node_type") + private String newClusterNodeTypeId = DatabricksJobSpecFactory.DEFAULT_NODE_TYPE_ID; + + /** Worker count for classic job clusters (string for variables). */ + @HopMetadataProperty(key = "new_cluster_num_workers") + private String newClusterNumWorkers = + Integer.toString(DatabricksJobSpecFactory.DEFAULT_NUM_WORKERS); + + /** + * Optional full {@code new_cluster} JSON object. When set (and cluster field is {@code + * new_cluster}), overrides the structured spark_version / node_type_id / num_workers fields. + */ + @HopMetadataProperty(key = "new_cluster_json") + private String newClusterJson; + + /** + * Serverless environment_key (default {@code default}). Used when cluster field is serverless. + */ + @HopMetadataProperty(key = "environment_key") + private String environmentKey = DatabricksJobSpecFactory.DEFAULT_ENVIRONMENT_KEY; + + /** + * Serverless environment {@code spec.client} version (e.g. {@code 4} for Spark 4.x). Used when + * cluster field is serverless. + */ + @HopMetadataProperty(key = "environment_client") + private String environmentClient = DatabricksJobSpecFactory.DEFAULT_ENVIRONMENT_CLIENT; + + /** Job name when creating a new job (deploy mode). */ + @HopMetadataProperty(key = "job_name") + private String jobName; + + /** + * When true and job_id is set, reset the existing job after upload; otherwise create a new job + * (job_id empty) or run-now only if create fails... actually: update if job_id non-empty. + */ + @HopMetadataProperty(key = "update_existing_job") + private boolean updateExistingJob; + + /** {@link #WAIT_WAIT} or {@link #WAIT_FIRE_AND_FORGET}. */ + @HopMetadataProperty(key = "wait_mode") + private String waitMode = WAIT_WAIT; + + /** Seconds; 0 = no timeout. */ + @HopMetadataProperty(key = "timeout_seconds") + private String timeoutSeconds = "3600"; + + @HopMetadataProperty(key = "poll_seconds") + private String pollIntervalSeconds = "15"; + + @HopMetadataProperty(key = "var_job_id") + private String resultVariableJobId = "DatabricksJobId"; + + @HopMetadataProperty(key = "var_run_id") + private String resultVariableRunId = "DatabricksRunId"; + + @HopMetadataProperty(key = "var_status") + private String resultVariableStatus = "DatabricksStatus"; + + @HopMetadataProperty(key = "var_page_url") + private String resultVariablePageUrl = "DatabricksRunPageUrl"; + + @HopMetadataProperty(key = "var_error") + private String resultVariableError = "DatabricksError"; + + /** Optional factory for tests. */ + private transient ClientFactory clientFactory = RestDatabricksJobsClient::create; + + @FunctionalInterface + public interface ClientFactory { + DatabricksJobsClient create(DatabricksConnection connection, IVariables variables) + throws HopException; + } + + public ActionDatabricksJobRun(String name) { + super(name, ""); + } + + public ActionDatabricksJobRun() { + this(""); + } + + @Override + public Object clone() { + return super.clone(); + } + + public void setClientFactory(ClientFactory clientFactory) { + this.clientFactory = clientFactory != null ? clientFactory : RestDatabricksJobsClient::create; + } + + @Override + public Result execute(Result previousResult, int nr) { + Result result = previousResult; + result.setResult(false); + result.setNrErrors(1); + clearResultVariables(); + + try { + String connName = resolve(connectionName); + if (Utils.isEmpty(connName)) { + throw new HopException( + BaseMessages.getString(PKG, "ActionDatabricksJobRun.Error.NoConnection")); + } + IHopMetadataProvider metadataProvider = getMetadataProvider(); + if (metadataProvider == null) { + throw new HopException( + BaseMessages.getString(PKG, "ActionDatabricksJobRun.Error.NoMetadataProvider")); + } + DatabricksConnection connection = + metadataProvider.getSerializer(DatabricksConnection.class).load(connName); + if (connection == null) { + throw new HopException( + BaseMessages.getString( + PKG, "ActionDatabricksJobRun.Error.ConnectionNotFound", connName)); + } + + try (DatabricksJobsClient client = clientFactory.create(connection, this)) { + long runId; + Long jobIdValue = null; + String mode = StringUtils.defaultIfBlank(runMode, MODE_RUN_EXISTING).trim(); + + if (MODE_DEPLOY_AND_RUN.equalsIgnoreCase(mode)) { + DeployOutcome outcome = deployAndRun(client, metadataProvider); + jobIdValue = outcome.jobId(); + runId = outcome.runId(); + } else if (MODE_SUBMIT_ONCE.equalsIgnoreCase(mode)) { + String json = resolve(submitRunJson); + if (Utils.isEmpty(json)) { + throw new HopException( + BaseMessages.getString(PKG, "ActionDatabricksJobRun.Error.NoSubmitJson")); + } + runId = client.submitRun(json); + if (isDetailed()) { + logDetailed("Submitted one-time Databricks run, run_id=" + runId); + } + } else { + String jobIdStr = resolve(jobId); + if (Utils.isEmpty(jobIdStr)) { + throw new HopException( + BaseMessages.getString(PKG, "ActionDatabricksJobRun.Error.NoJobId")); + } + long jid = Long.parseLong(jobIdStr.trim()); + jobIdValue = jid; + runId = client.runNow(jid, Map.of()); + if (isDetailed()) { + logDetailed("Triggered Databricks job_id=" + jid + ", run_id=" + runId); + } + } + + setResultVariable(resultVariableJobId, jobIdValue != null ? Long.toString(jobIdValue) : ""); + setResultVariable(resultVariableRunId, Long.toString(runId)); + setResultVariable(resultVariableStatus, DatabricksRunLifeCycleState.PENDING.name()); + setResultVariable(resultVariableError, ""); + + String wait = StringUtils.defaultIfBlank(waitMode, WAIT_WAIT).trim(); + if (WAIT_FIRE_AND_FORGET.equalsIgnoreCase(wait)) { + result.setNrErrors(0); + result.setResult(true); + return result; + } + + DatabricksRunStatus status = waitForRun(client, runId); + applyStatusVariables(status, jobIdValue); + + if (status.isSuccess()) { + result.setNrErrors(0); + result.setResult(true); + } else if (status.getLifeCycleState() == DatabricksRunLifeCycleState.TERMINATED + && "CANCELED".equalsIgnoreCase(status.getResultState())) { + result.setNrErrors(1); + result.setResult(false); + logError("Databricks run canceled: " + Const.NVL(status.getStateMessage(), "")); + } else { + result.setNrErrors(1); + result.setResult(false); + String msg = + Const.NVL( + status.getStateMessage(), + "Databricks run ended with status " + status.toStatusVariable()); + setResultVariable(resultVariableError, msg); + logError(msg); + } + } + } catch (Exception e) { + result.setNrErrors(1); + result.setResult(false); + setResultVariable( + resultVariableError, e.getMessage() != null ? e.getMessage() : e.toString()); + logError(BaseMessages.getString(PKG, "ActionDatabricksJobRun.Error.Execute"), e); + } + + return result; + } + + private record DeployOutcome(long jobId, long runId) {} + + private DeployOutcome deployAndRun( + DatabricksJobsClient client, IHopMetadataProvider metadataProvider) throws HopException { + String cluster = resolve(existingClusterId); + if (Utils.isEmpty(cluster)) { + throw new HopException( + BaseMessages.getString(PKG, "ActionDatabricksJobRun.Error.NoClusterId")); + } + DatabricksJobSpecFactory.ClusterTarget clusterTarget = + DatabricksJobSpecFactory.resolveClusterTarget( + cluster, + resolve(newClusterSparkVersion), + resolve(newClusterNodeTypeId), + resolve(newClusterNumWorkers), + resolve(newClusterJson), + resolve(environmentKey), + resolve(environmentClient)); + String name = resolve(jobName); + if (Utils.isEmpty(name)) { + name = "hop-" + resolve(runConfigurationName); + } + + HopSparkDeployHelper.DeployOptions deployOptions = + new HopSparkDeployHelper.DeployOptions( + uploadProjectPackage, projectHome, projectPackageFile, environmentConfigFile); + HopSparkDeployHelper.DeployedArtifacts artifacts = + HopSparkDeployHelper.deploy( + client, + metadataProvider, + this, + getLogChannel(), + fatJarPath, + pipelineFilename, + runConfigurationName, + dbfsBasePath, + deployOptions); + + long jid; + String jobIdStr = resolve(jobId); + if (updateExistingJob && StringUtils.isNotBlank(jobIdStr)) { + jid = Long.parseLong(jobIdStr.trim()); + String resetJson = + DatabricksJobSpecFactory.buildResetJobJson( + jid, name, clusterTarget, artifacts.jarDbfs(), artifacts.launch()); + client.resetJob(resetJson); + if (isBasic()) { + logBasic("Updated Databricks job_id=" + jid); + } + } else { + String createJson = + DatabricksJobSpecFactory.buildCreateJobJson( + name, clusterTarget, artifacts.jarDbfs(), artifacts.launch()); + jid = client.createJob(createJson); + if (isBasic()) { + logBasic("Created Databricks job_id=" + jid); + } + } + + long rid = client.runNow(jid, Map.of()); + if (isBasic()) { + logBasic("Started Databricks job_id=" + jid + " run_id=" + rid); + } + return new DeployOutcome(jid, rid); + } + + private DatabricksRunStatus waitForRun(DatabricksJobsClient client, long runId) + throws HopException, InterruptedException { + int timeoutSec = Const.toInt(resolve(timeoutSeconds), 3600); + int pollSec = Const.toInt(resolve(pollIntervalSeconds), 15); + return DatabricksRunWaiter.waitFor( + client, + runId, + timeoutSec, + pollSec, + true, + new DatabricksRunWaiter.Hooks() { + @Override + public boolean isStopped() { + return parentWorkflow != null && parentWorkflow.isStopped(); + } + + @Override + public void onStatus(DatabricksRunStatus status) { + applyStatusVariables(status, status.getJobId()); + } + + @Override + public void logDetailed(String message) { + if (isDetailed()) { + ActionDatabricksJobRun.this.logDetailed(message); + } + } + + @Override + public void logError(String message) { + ActionDatabricksJobRun.this.logError(message); + } + }); + } + + private void applyStatusVariables(DatabricksRunStatus status, Long jobIdFallback) { + if (status.getJobId() != null) { + setResultVariable(resultVariableJobId, Long.toString(status.getJobId())); + } else if (jobIdFallback != null) { + setResultVariable(resultVariableJobId, Long.toString(jobIdFallback)); + } + setResultVariable(resultVariableRunId, Long.toString(status.getRunId())); + setResultVariable(resultVariableStatus, status.toStatusVariable()); + if (status.getRunPageUrl() != null) { + setResultVariable(resultVariablePageUrl, status.getRunPageUrl()); + } + } + + private void clearResultVariables() { + setResultVariable(resultVariableJobId, ""); + setResultVariable(resultVariableRunId, ""); + setResultVariable(resultVariableStatus, ""); + setResultVariable(resultVariablePageUrl, ""); + setResultVariable(resultVariableError, ""); + } + + private void setResultVariable(String name, String value) { + if (Utils.isEmpty(name)) { + return; + } + String varName = resolve(name); + String varValue = value == null ? "" : value; + setVariable(varName, varValue); + IWorkflowEngine parent = getParentWorkflow(); + if (parent != null) { + parent.setVariable(varName, varValue); + IWorkflowEngine p = parent.getParentWorkflow(); + while (p != null) { + p.setVariable(varName, varValue); + p = p.getParentWorkflow(); + } + } + } + + @Override + public boolean isEvaluation() { + return true; + } + + @Override + public boolean isUnconditional() { + return false; + } + + @Override + public void check( + List remarks, + WorkflowMeta workflowMeta, + IVariables variables, + IHopMetadataProvider metadataProvider) { + ActionValidatorUtils.andValidator() + .validate( + this, + "connectionName", + remarks, + AndValidator.putValidators(ActionValidatorUtils.notBlankValidator())); + } + + /** + * @return The objects referenced in the transform, like a a pipeline, a workflow, a mapper, a + * reducer, a combiner, ... + */ + @Override + public String[] getReferencedObjectDescriptions() { + return new String[] { + BaseMessages.getString(PKG, "ActionDatabricksJobRun.ReferencedObject.Description"), + }; + } + + private boolean isPipelineDefined() { + return StringUtils.isNotEmpty(pipelineFilename); + } + + @Override + public boolean[] isReferencedObjectEnabled() { + return new boolean[] { + isPipelineDefined(), + }; + } + + /** + * Load the referenced object + * + * @param index the referenced object index to load (in case there are multiple references) + * @param metadataProvider metadataProvider + * @param variables the variable variables to use + * @return the referenced object once loaded + * @throws HopException + */ + @Override + public IHasFilename loadReferencedObject( + int index, IHopMetadataProvider metadataProvider, IVariables variables) throws HopException { + return getPipelineMeta(metadataProvider, variables); + } + + public PipelineMeta getPipelineMeta(IHopMetadataProvider metadataProvider, IVariables variables) + throws HopException { + try { + PipelineMeta pipelineMeta = null; + CurrentDirectoryResolver directoryResolver = new CurrentDirectoryResolver(); + IVariables tmpSpace = + directoryResolver.resolveCurrentDirectory(variables, parentWorkflow, getFilename()); + + pipelineMeta = + new PipelineMeta(tmpSpace.resolve(getPipelineFilename()), metadataProvider, this); + pipelineMeta.setMetadataProvider(metadataProvider); + return pipelineMeta; + } catch (final HopException ke) { + // if we get a HopException, simply re-throw it + throw ke; + } catch (Exception e) { + throw new HopException( + BaseMessages.getString(PKG, "ActionDatabricksJobRun.Exception.MetaDataLoad"), e); + } + } +} diff --git a/plugins/tech/databricks/src/main/java/org/apache/hop/workflow/actions/databricks/ActionDatabricksJobRunDialog.java b/plugins/tech/databricks/src/main/java/org/apache/hop/workflow/actions/databricks/ActionDatabricksJobRunDialog.java new file mode 100644 index 00000000000..fe5ee71cc2f --- /dev/null +++ b/plugins/tech/databricks/src/main/java/org/apache/hop/workflow/actions/databricks/ActionDatabricksJobRunDialog.java @@ -0,0 +1,871 @@ +/* + * 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.hop.workflow.actions.databricks; + +import org.apache.hop.core.Const; +import org.apache.hop.core.util.Utils; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.databricks.metadata.DatabricksConnection; +import org.apache.hop.i18n.BaseMessages; +import org.apache.hop.pipeline.config.PipelineRunConfiguration; +import org.apache.hop.ui.core.PropsUi; +import org.apache.hop.ui.core.dialog.BaseDialog; +import org.apache.hop.ui.core.dialog.ErrorDialog; +import org.apache.hop.ui.core.dialog.MessageBox; +import org.apache.hop.ui.core.widget.MetaSelectionLine; +import org.apache.hop.ui.core.widget.TextVar; +import org.apache.hop.ui.hopgui.HopGui; +import org.apache.hop.ui.hopgui.file.pipeline.HopPipelineFileType; +import org.apache.hop.ui.pipeline.transform.BaseTransformDialog; +import org.apache.hop.ui.workflow.action.ActionDialog; +import org.apache.hop.ui.workflow.dialog.WorkflowDialog; +import org.apache.hop.workflow.WorkflowMeta; +import org.apache.hop.workflow.action.IAction; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.CCombo; +import org.eclipse.swt.custom.CTabFolder; +import org.eclipse.swt.custom.CTabItem; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +/** Dialog for {@link ActionDatabricksJobRun}. */ +public class ActionDatabricksJobRunDialog extends ActionDialog { + private static final Class PKG = ActionDatabricksJobRun.class; + + private ActionDatabricksJobRun action; + private boolean changed; + + private Text wName; + private MetaSelectionLine wConnection; + private CCombo wRunMode; + private TextVar wJobId; + private TextVar wJobName; + private Button wUpdateJob; + private StyledText wSubmitJson; + private TextVar wFatJar; + private Button wbFatJarBrowse; + private TextVar wPipeline; + private Button wbPipelineBrowse; + private Button wbPipelineOpen; + private MetaSelectionLine wRunConfig; + private TextVar wDbfsBase; + private Button wUploadProjectPackage; + private TextVar wProjectHome; + private TextVar wProjectPackageFile; + private Button wbProjectPackageBrowse; + private TextVar wEnvironmentConfigFile; + private Button wbEnvironmentConfigBrowse; + private TextVar wClusterId; + private TextVar wNewClusterSparkVersion; + private TextVar wNewClusterNodeType; + private TextVar wNewClusterNumWorkers; + private StyledText wNewClusterJson; + private TextVar wEnvironmentKey; + private TextVar wEnvironmentClient; + private CCombo wWaitMode; + private TextVar wTimeout; + private TextVar wPoll; + private TextVar wVarJobId; + private TextVar wVarRunId; + private TextVar wVarStatus; + private TextVar wVarPageUrl; + private TextVar wVarError; + + private Label wlJobId; + private Label wlSubmitJson; + + public ActionDatabricksJobRunDialog( + Shell parent, + ActionDatabricksJobRun action, + WorkflowMeta workflowMeta, + IVariables variables) { + super(parent, workflowMeta, variables); + this.action = action; + if (this.action.getName() == null) { + this.action.setName(BaseMessages.getString(PKG, "ActionDatabricksJobRun.Name.Default")); + } + } + + @Override + public IAction open() { + shell = new Shell(getParent(), SWT.DIALOG_TRIM | SWT.MIN | SWT.MAX | SWT.RESIZE); + PropsUi.setLook(shell); + WorkflowDialog.setShellImage(shell, action); + + FormLayout formLayout = new FormLayout(); + formLayout.marginWidth = PropsUi.getFormMargin(); + formLayout.marginHeight = PropsUi.getFormMargin(); + shell.setLayout(formLayout); + shell.setText(BaseMessages.getString(PKG, "ActionDatabricksJobRun.Title")); + + int middle = props.getMiddlePct(); + int margin = PropsUi.getMargin(); + changed = action.hasChanged(); + ModifyListener lsMod = e -> action.setChanged(); + + Button wOk = new Button(shell, SWT.PUSH); + wOk.setText(BaseMessages.getString(PKG, "System.Button.OK")); + wOk.addListener(SWT.Selection, e -> ok()); + Button wCancel = new Button(shell, SWT.PUSH); + wCancel.setText(BaseMessages.getString(PKG, "System.Button.Cancel")); + wCancel.addListener(SWT.Selection, e -> cancel()); + BaseTransformDialog.positionBottomButtons(shell, new Button[] {wOk, wCancel}, margin, null); + + Label wlName = new Label(shell, SWT.RIGHT); + wlName.setText(BaseMessages.getString(PKG, "ActionDatabricksJobRun.Name.Label")); + PropsUi.setLook(wlName); + FormData fdlName = new FormData(); + fdlName.left = new FormAttachment(0, 0); + fdlName.right = new FormAttachment(middle, -margin); + fdlName.top = new FormAttachment(0, margin); + wlName.setLayoutData(fdlName); + wName = new Text(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wName); + wName.addModifyListener(lsMod); + FormData fdName = new FormData(); + fdName.left = new FormAttachment(middle, 0); + fdName.top = new FormAttachment(wlName, 0, SWT.CENTER); + fdName.right = new FormAttachment(100, 0); + wName.setLayoutData(fdName); + + CTabFolder wTabFolder = new CTabFolder(shell, SWT.BORDER); + PropsUi.setLook(wTabFolder); + FormData fdTab = new FormData(); + fdTab.left = new FormAttachment(0, 0); + fdTab.top = new FormAttachment(wName, margin); + fdTab.right = new FormAttachment(100, 0); + fdTab.bottom = new FormAttachment(wOk, -margin); + wTabFolder.setLayoutData(fdTab); + + addJobTab(wTabFolder, middle, margin, lsMod); + addDeployTab(wTabFolder, middle, margin, lsMod); + addPackageTab(wTabFolder, middle, margin, lsMod); + addComputeTab(wTabFolder, middle, margin, lsMod); + addRunTab(wTabFolder, middle, margin, lsMod); + + wTabFolder.setSelection(0); + getData(); + enableModeFields(); + wName.setFocus(); + + BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel()); + return action; + } + + /** Connection, mode, job id/name, update flag, submit JSON. */ + private void addJobTab(CTabFolder wTabFolder, int middle, int margin, ModifyListener lsMod) { + CTabItem tabJob = new CTabItem(wTabFolder, SWT.NONE); + tabJob.setText(BaseMessages.getString(PKG, "ActionDatabricksJobRun.Tab.Job")); + Composite wJobComp = new Composite(wTabFolder, SWT.NONE); + PropsUi.setLook(wJobComp); + wJobComp.setLayout(new FormLayout()); + tabJob.setControl(wJobComp); + + wConnection = + new MetaSelectionLine<>( + variables, + metadataProvider, + DatabricksConnection.class, + wJobComp, + SWT.NONE, + BaseMessages.getString(PKG, "ActionDatabricksJobRun.Connection.Label"), + BaseMessages.getString(PKG, "ActionDatabricksJobRun.Connection.Tooltip")); + PropsUi.setLook(wConnection); + FormData fdConn = new FormData(); + fdConn.left = new FormAttachment(0, 0); + fdConn.top = new FormAttachment(0, margin); + fdConn.right = new FormAttachment(100, 0); + wConnection.setLayoutData(fdConn); + try { + wConnection.fillItems(); + } catch (Exception e) { + // ignore empty metadata + } + wConnection.addModifyListener(lsMod); + + Label wlRunMode = new Label(wJobComp, SWT.RIGHT); + wlRunMode.setText(BaseMessages.getString(PKG, "ActionDatabricksJobRun.RunMode.Label")); + PropsUi.setLook(wlRunMode); + FormData fdlRunMode = new FormData(); + fdlRunMode.left = new FormAttachment(0, 0); + fdlRunMode.right = new FormAttachment(middle, -margin); + fdlRunMode.top = new FormAttachment(wConnection, margin); + wlRunMode.setLayoutData(fdlRunMode); + wRunMode = new CCombo(wJobComp, SWT.BORDER | SWT.READ_ONLY); + wRunMode.setItems( + new String[] { + BaseMessages.getString(PKG, "ActionDatabricksJobRun.RunMode.RunExisting"), + BaseMessages.getString(PKG, "ActionDatabricksJobRun.RunMode.SubmitOnce"), + BaseMessages.getString(PKG, "ActionDatabricksJobRun.RunMode.DeployAndRun") + }); + PropsUi.setLook(wRunMode); + FormData fdRunMode = new FormData(); + fdRunMode.left = new FormAttachment(middle, 0); + fdRunMode.top = new FormAttachment(wConnection, margin); + fdRunMode.right = new FormAttachment(100, 0); + wRunMode.setLayoutData(fdRunMode); + wRunMode.addModifyListener(lsMod); + wRunMode.addListener(SWT.Selection, e -> enableModeFields()); + + wlJobId = new Label(wJobComp, SWT.RIGHT); + wlJobId.setText(BaseMessages.getString(PKG, "ActionDatabricksJobRun.JobId.Label")); + PropsUi.setLook(wlJobId); + FormData fdlJobId = new FormData(); + fdlJobId.left = new FormAttachment(0, 0); + fdlJobId.right = new FormAttachment(middle, -margin); + fdlJobId.top = new FormAttachment(wRunMode, margin); + wlJobId.setLayoutData(fdlJobId); + wJobId = new TextVar(variables, wJobComp, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wJobId); + wJobId.addModifyListener(lsMod); + FormData fdJobId = new FormData(); + fdJobId.left = new FormAttachment(middle, 0); + fdJobId.top = new FormAttachment(wRunMode, margin); + fdJobId.right = new FormAttachment(100, 0); + wJobId.setLayoutData(fdJobId); + + wJobName = + addLabeledTextVar( + wJobComp, wJobId, middle, margin, lsMod, "ActionDatabricksJobRun.JobName.Label"); + + wUpdateJob = new Button(wJobComp, SWT.CHECK); + PropsUi.setLook(wUpdateJob); + wUpdateJob.setText(BaseMessages.getString(PKG, "ActionDatabricksJobRun.UpdateJob.Label")); + FormData fdUpdate = new FormData(); + fdUpdate.left = new FormAttachment(middle, 0); + fdUpdate.top = new FormAttachment(wJobName, margin); + wUpdateJob.setLayoutData(fdUpdate); + wUpdateJob.addListener(SWT.Selection, e -> action.setChanged()); + + wlSubmitJson = new Label(wJobComp, SWT.RIGHT); + wlSubmitJson.setText(BaseMessages.getString(PKG, "ActionDatabricksJobRun.SubmitJson.Label")); + PropsUi.setLook(wlSubmitJson); + FormData fdlSubmit = new FormData(); + fdlSubmit.left = new FormAttachment(0, 0); + fdlSubmit.right = new FormAttachment(middle, -margin); + fdlSubmit.top = new FormAttachment(wUpdateJob, margin); + wlSubmitJson.setLayoutData(fdlSubmit); + wSubmitJson = + new StyledText(wJobComp, SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL); + PropsUi.setLook(wSubmitJson); + wSubmitJson.addModifyListener(lsMod); + FormData fdSubmit = new FormData(); + fdSubmit.left = new FormAttachment(middle, 0); + fdSubmit.top = new FormAttachment(wUpdateJob, margin); + fdSubmit.right = new FormAttachment(100, 0); + fdSubmit.bottom = new FormAttachment(100, -margin); + fdSubmit.height = 80; + wSubmitJson.setLayoutData(fdSubmit); + } + + /** Fat jar, pipeline, run configuration, upload base. */ + private void addDeployTab(CTabFolder wTabFolder, int middle, int margin, ModifyListener lsMod) { + CTabItem tabDeploy = new CTabItem(wTabFolder, SWT.NONE); + tabDeploy.setText(BaseMessages.getString(PKG, "ActionDatabricksJobRun.Tab.Deploy")); + Composite wDeployComp = new Composite(wTabFolder, SWT.NONE); + PropsUi.setLook(wDeployComp); + wDeployComp.setLayout(new FormLayout()); + tabDeploy.setControl(wDeployComp); + + Label wlFatJar = new Label(wDeployComp, SWT.RIGHT); + wlFatJar.setText(BaseMessages.getString(PKG, "ActionDatabricksJobRun.FatJar.Label")); + PropsUi.setLook(wlFatJar); + FormData fdlFatJar = new FormData(); + fdlFatJar.left = new FormAttachment(0, 0); + fdlFatJar.right = new FormAttachment(middle, -margin); + fdlFatJar.top = new FormAttachment(0, margin); + wlFatJar.setLayoutData(fdlFatJar); + wbFatJarBrowse = new Button(wDeployComp, SWT.PUSH); + PropsUi.setLook(wbFatJarBrowse); + wbFatJarBrowse.setText(BaseMessages.getString(PKG, "System.Button.Browse")); + FormData fdbFatJar = new FormData(); + fdbFatJar.right = new FormAttachment(100, 0); + fdbFatJar.top = new FormAttachment(wlFatJar, 0, SWT.CENTER); + wbFatJarBrowse.setLayoutData(fdbFatJar); + wbFatJarBrowse.addListener(SWT.Selection, e -> browseFatJar()); + wFatJar = new TextVar(variables, wDeployComp, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wFatJar); + wFatJar.addModifyListener(lsMod); + FormData fdFatJar = new FormData(); + fdFatJar.left = new FormAttachment(middle, 0); + fdFatJar.top = new FormAttachment(wlFatJar, 0, SWT.CENTER); + fdFatJar.right = new FormAttachment(wbFatJarBrowse, -margin); + wFatJar.setLayoutData(fdFatJar); + + Label wlPipeline = new Label(wDeployComp, SWT.RIGHT); + wlPipeline.setText(BaseMessages.getString(PKG, "ActionDatabricksJobRun.Pipeline.Label")); + PropsUi.setLook(wlPipeline); + FormData fdlPipeline = new FormData(); + fdlPipeline.left = new FormAttachment(0, 0); + fdlPipeline.right = new FormAttachment(middle, -margin); + fdlPipeline.top = new FormAttachment(wFatJar, margin); + wlPipeline.setLayoutData(fdlPipeline); + wbPipelineOpen = new Button(wDeployComp, SWT.PUSH); + PropsUi.setLook(wbPipelineOpen); + wbPipelineOpen.setText(BaseMessages.getString(PKG, "System.Button.Open")); + FormData fdbPipelineOpen = new FormData(); + fdbPipelineOpen.right = new FormAttachment(100, 0); + fdbPipelineOpen.top = new FormAttachment(wlPipeline, 0, SWT.CENTER); + wbPipelineOpen.setLayoutData(fdbPipelineOpen); + wbPipelineOpen.addListener(SWT.Selection, e -> openPipeline()); + wbPipelineBrowse = new Button(wDeployComp, SWT.PUSH); + PropsUi.setLook(wbPipelineBrowse); + wbPipelineBrowse.setText(BaseMessages.getString(PKG, "System.Button.Browse")); + FormData fdbPipelineBrowse = new FormData(); + fdbPipelineBrowse.right = new FormAttachment(wbPipelineOpen, -margin); + fdbPipelineBrowse.top = new FormAttachment(wlPipeline, 0, SWT.CENTER); + wbPipelineBrowse.setLayoutData(fdbPipelineBrowse); + wbPipelineBrowse.addListener(SWT.Selection, e -> browsePipeline()); + wPipeline = new TextVar(variables, wDeployComp, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wPipeline); + wPipeline.addModifyListener(lsMod); + FormData fdPipeline = new FormData(); + fdPipeline.left = new FormAttachment(middle, 0); + fdPipeline.top = new FormAttachment(wlPipeline, 0, SWT.CENTER); + fdPipeline.right = new FormAttachment(wbPipelineBrowse, -margin); + wPipeline.setLayoutData(fdPipeline); + + wRunConfig = + new MetaSelectionLine<>( + variables, + metadataProvider, + PipelineRunConfiguration.class, + wDeployComp, + SWT.NONE, + BaseMessages.getString(PKG, "ActionDatabricksJobRun.RunConfig.Label"), + BaseMessages.getString(PKG, "ActionDatabricksJobRun.RunConfig.Tooltip")); + PropsUi.setLook(wRunConfig); + FormData fdRunConfig = new FormData(); + fdRunConfig.left = new FormAttachment(0, 0); + fdRunConfig.top = new FormAttachment(wPipeline, margin); + fdRunConfig.right = new FormAttachment(100, 0); + wRunConfig.setLayoutData(fdRunConfig); + try { + wRunConfig.fillItems(); + } catch (Exception e) { + // ignore empty metadata + } + wRunConfig.addModifyListener(lsMod); + + wDbfsBase = + addLabeledTextVar( + wDeployComp, + wRunConfig, + middle, + margin, + lsMod, + "ActionDatabricksJobRun.DbfsBase.Label"); + } + + /** Optional Spark project package + environment config. */ + private void addPackageTab(CTabFolder wTabFolder, int middle, int margin, ModifyListener lsMod) { + CTabItem tabPackage = new CTabItem(wTabFolder, SWT.NONE); + tabPackage.setText(BaseMessages.getString(PKG, "ActionDatabricksJobRun.Tab.Package")); + Composite wPackageComp = new Composite(wTabFolder, SWT.NONE); + PropsUi.setLook(wPackageComp); + wPackageComp.setLayout(new FormLayout()); + tabPackage.setControl(wPackageComp); + + wUploadProjectPackage = new Button(wPackageComp, SWT.CHECK); + PropsUi.setLook(wUploadProjectPackage); + wUploadProjectPackage.setText( + BaseMessages.getString(PKG, "ActionDatabricksJobRun.UploadProjectPackage.Label")); + wUploadProjectPackage.setToolTipText( + BaseMessages.getString(PKG, "ActionDatabricksJobRun.UploadProjectPackage.Tooltip")); + FormData fdUploadPkg = new FormData(); + fdUploadPkg.left = new FormAttachment(middle, 0); + fdUploadPkg.top = new FormAttachment(0, margin); + fdUploadPkg.right = new FormAttachment(100, 0); + wUploadProjectPackage.setLayoutData(fdUploadPkg); + wUploadProjectPackage.addListener(SWT.Selection, e -> action.setChanged()); + + wProjectHome = + addLabeledTextVar( + wPackageComp, + wUploadProjectPackage, + middle, + margin, + lsMod, + "ActionDatabricksJobRun.ProjectHome.Label"); + wProjectHome.setToolTipText( + BaseMessages.getString(PKG, "ActionDatabricksJobRun.ProjectHome.Tooltip")); + + Label wlProjectPackage = new Label(wPackageComp, SWT.RIGHT); + wlProjectPackage.setText( + BaseMessages.getString(PKG, "ActionDatabricksJobRun.ProjectPackageFile.Label")); + PropsUi.setLook(wlProjectPackage); + FormData fdlProjectPackage = new FormData(); + fdlProjectPackage.left = new FormAttachment(0, 0); + fdlProjectPackage.right = new FormAttachment(middle, -margin); + fdlProjectPackage.top = new FormAttachment(wProjectHome, margin); + wlProjectPackage.setLayoutData(fdlProjectPackage); + wbProjectPackageBrowse = new Button(wPackageComp, SWT.PUSH); + PropsUi.setLook(wbProjectPackageBrowse); + wbProjectPackageBrowse.setText(BaseMessages.getString(PKG, "System.Button.Browse")); + FormData fdbProjectPackage = new FormData(); + fdbProjectPackage.right = new FormAttachment(100, 0); + fdbProjectPackage.top = new FormAttachment(wlProjectPackage, 0, SWT.CENTER); + wbProjectPackageBrowse.setLayoutData(fdbProjectPackage); + wbProjectPackageBrowse.addListener(SWT.Selection, e -> browseProjectPackage()); + wProjectPackageFile = new TextVar(variables, wPackageComp, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wProjectPackageFile); + wProjectPackageFile.addModifyListener(lsMod); + wProjectPackageFile.setToolTipText( + BaseMessages.getString(PKG, "ActionDatabricksJobRun.ProjectPackageFile.Tooltip")); + FormData fdProjectPackage = new FormData(); + fdProjectPackage.left = new FormAttachment(middle, 0); + fdProjectPackage.top = new FormAttachment(wlProjectPackage, 0, SWT.CENTER); + fdProjectPackage.right = new FormAttachment(wbProjectPackageBrowse, -margin); + wProjectPackageFile.setLayoutData(fdProjectPackage); + + Label wlEnvConfig = new Label(wPackageComp, SWT.RIGHT); + wlEnvConfig.setText( + BaseMessages.getString(PKG, "ActionDatabricksJobRun.EnvironmentConfigFile.Label")); + PropsUi.setLook(wlEnvConfig); + FormData fdlEnvConfig = new FormData(); + fdlEnvConfig.left = new FormAttachment(0, 0); + fdlEnvConfig.right = new FormAttachment(middle, -margin); + fdlEnvConfig.top = new FormAttachment(wProjectPackageFile, margin); + wlEnvConfig.setLayoutData(fdlEnvConfig); + wbEnvironmentConfigBrowse = new Button(wPackageComp, SWT.PUSH); + PropsUi.setLook(wbEnvironmentConfigBrowse); + wbEnvironmentConfigBrowse.setText(BaseMessages.getString(PKG, "System.Button.Browse")); + FormData fdbEnvConfig = new FormData(); + fdbEnvConfig.right = new FormAttachment(100, 0); + fdbEnvConfig.top = new FormAttachment(wlEnvConfig, 0, SWT.CENTER); + wbEnvironmentConfigBrowse.setLayoutData(fdbEnvConfig); + wbEnvironmentConfigBrowse.addListener(SWT.Selection, e -> browseEnvironmentConfig()); + wEnvironmentConfigFile = + new TextVar(variables, wPackageComp, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wEnvironmentConfigFile); + wEnvironmentConfigFile.addModifyListener(lsMod); + wEnvironmentConfigFile.setToolTipText( + BaseMessages.getString(PKG, "ActionDatabricksJobRun.EnvironmentConfigFile.Tooltip")); + FormData fdEnvConfig = new FormData(); + fdEnvConfig.left = new FormAttachment(middle, 0); + fdEnvConfig.top = new FormAttachment(wlEnvConfig, 0, SWT.CENTER); + fdEnvConfig.right = new FormAttachment(wbEnvironmentConfigBrowse, -margin); + wEnvironmentConfigFile.setLayoutData(fdEnvConfig); + } + + /** Cluster / new_cluster / serverless. */ + private void addComputeTab(CTabFolder wTabFolder, int middle, int margin, ModifyListener lsMod) { + CTabItem tabCompute = new CTabItem(wTabFolder, SWT.NONE); + tabCompute.setText(BaseMessages.getString(PKG, "ActionDatabricksJobRun.Tab.Compute")); + Composite wComputeComp = new Composite(wTabFolder, SWT.NONE); + PropsUi.setLook(wComputeComp); + wComputeComp.setLayout(new FormLayout()); + tabCompute.setControl(wComputeComp); + + wClusterId = + addLabeledTextVar( + wComputeComp, + wComputeComp, + middle, + margin, + lsMod, + "ActionDatabricksJobRun.ClusterId.Label", + true); + wClusterId.setToolTipText( + BaseMessages.getString(PKG, "ActionDatabricksJobRun.ClusterId.Tooltip")); + wNewClusterSparkVersion = + addLabeledTextVar( + wComputeComp, + wClusterId, + middle, + margin, + lsMod, + "ActionDatabricksJobRun.NewClusterSparkVersion.Label"); + wNewClusterSparkVersion.setToolTipText( + BaseMessages.getString(PKG, "ActionDatabricksJobRun.NewClusterSparkVersion.Tooltip")); + wNewClusterNodeType = + addLabeledTextVar( + wComputeComp, + wNewClusterSparkVersion, + middle, + margin, + lsMod, + "ActionDatabricksJobRun.NewClusterNodeType.Label"); + wNewClusterNodeType.setToolTipText( + BaseMessages.getString(PKG, "ActionDatabricksJobRun.NewClusterNodeType.Tooltip")); + wNewClusterNumWorkers = + addLabeledTextVar( + wComputeComp, + wNewClusterNodeType, + middle, + margin, + lsMod, + "ActionDatabricksJobRun.NewClusterNumWorkers.Label"); + wNewClusterNumWorkers.setToolTipText( + BaseMessages.getString(PKG, "ActionDatabricksJobRun.NewClusterNumWorkers.Tooltip")); + + Label wlNewClusterJson = new Label(wComputeComp, SWT.RIGHT); + wlNewClusterJson.setText( + BaseMessages.getString(PKG, "ActionDatabricksJobRun.NewClusterJson.Label")); + PropsUi.setLook(wlNewClusterJson); + FormData fdlNewClusterJson = new FormData(); + fdlNewClusterJson.left = new FormAttachment(0, 0); + fdlNewClusterJson.right = new FormAttachment(middle, -margin); + fdlNewClusterJson.top = new FormAttachment(wNewClusterNumWorkers, margin); + wlNewClusterJson.setLayoutData(fdlNewClusterJson); + wNewClusterJson = + new StyledText( + wComputeComp, SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL); + PropsUi.setLook(wNewClusterJson); + wNewClusterJson.setToolTipText( + BaseMessages.getString(PKG, "ActionDatabricksJobRun.NewClusterJson.Tooltip")); + wNewClusterJson.addModifyListener(lsMod); + FormData fdNewClusterJson = new FormData(); + fdNewClusterJson.left = new FormAttachment(middle, 0); + fdNewClusterJson.top = new FormAttachment(wNewClusterNumWorkers, margin); + fdNewClusterJson.right = new FormAttachment(100, 0); + fdNewClusterJson.height = 80; + wNewClusterJson.setLayoutData(fdNewClusterJson); + + wEnvironmentKey = + addLabeledTextVar( + wComputeComp, + wNewClusterJson, + middle, + margin, + lsMod, + "ActionDatabricksJobRun.EnvironmentKey.Label"); + wEnvironmentKey.setToolTipText( + BaseMessages.getString(PKG, "ActionDatabricksJobRun.EnvironmentKey.Tooltip")); + wEnvironmentClient = + addLabeledTextVar( + wComputeComp, + wEnvironmentKey, + middle, + margin, + lsMod, + "ActionDatabricksJobRun.EnvironmentClient.Label"); + wEnvironmentClient.setToolTipText( + BaseMessages.getString(PKG, "ActionDatabricksJobRun.EnvironmentClient.Tooltip")); + } + + /** Wait mode, timeout, poll, result variables. */ + private void addRunTab(CTabFolder wTabFolder, int middle, int margin, ModifyListener lsMod) { + CTabItem tabRun = new CTabItem(wTabFolder, SWT.NONE); + tabRun.setText(BaseMessages.getString(PKG, "ActionDatabricksJobRun.Tab.Run")); + Composite wRunComp = new Composite(wTabFolder, SWT.NONE); + PropsUi.setLook(wRunComp); + wRunComp.setLayout(new FormLayout()); + tabRun.setControl(wRunComp); + + Label wlWait = new Label(wRunComp, SWT.RIGHT); + wlWait.setText(BaseMessages.getString(PKG, "ActionDatabricksJobRun.WaitMode.Label")); + PropsUi.setLook(wlWait); + FormData fdlWait = new FormData(); + fdlWait.left = new FormAttachment(0, 0); + fdlWait.right = new FormAttachment(middle, -margin); + fdlWait.top = new FormAttachment(0, margin); + wlWait.setLayoutData(fdlWait); + wWaitMode = new CCombo(wRunComp, SWT.BORDER | SWT.READ_ONLY); + wWaitMode.setItems( + new String[] { + BaseMessages.getString(PKG, "ActionDatabricksJobRun.WaitMode.Wait"), + BaseMessages.getString(PKG, "ActionDatabricksJobRun.WaitMode.FireAndForget") + }); + PropsUi.setLook(wWaitMode); + FormData fdWait = new FormData(); + fdWait.left = new FormAttachment(middle, 0); + fdWait.top = new FormAttachment(0, margin); + fdWait.right = new FormAttachment(100, 0); + wWaitMode.setLayoutData(fdWait); + wWaitMode.addModifyListener(lsMod); + + Control last = addTextVarRow(wRunComp, wWaitMode, middle, margin, lsMod, "Timeout"); + wTimeout = (TextVar) last; + last = addTextVarRow(wRunComp, wTimeout, middle, margin, lsMod, "Poll"); + wPoll = (TextVar) last; + last = addTextVarRow(wRunComp, wPoll, middle, margin, lsMod, "VarJobId"); + wVarJobId = (TextVar) last; + last = addTextVarRow(wRunComp, wVarJobId, middle, margin, lsMod, "VarRunId"); + wVarRunId = (TextVar) last; + last = addTextVarRow(wRunComp, wVarRunId, middle, margin, lsMod, "VarStatus"); + wVarStatus = (TextVar) last; + last = addTextVarRow(wRunComp, wVarStatus, middle, margin, lsMod, "VarPageUrl"); + wVarPageUrl = (TextVar) last; + last = addTextVarRow(wRunComp, wVarPageUrl, middle, margin, lsMod, "VarError"); + wVarError = (TextVar) last; + } + + private TextVar addLabeledTextVar( + Composite parent, + Control above, + int middle, + int margin, + ModifyListener lsMod, + String labelKey) { + return addLabeledTextVar(parent, above, middle, margin, lsMod, labelKey, false); + } + + /** + * @param firstOnTab when true, attach top to parent margin instead of {@code above} + */ + private TextVar addLabeledTextVar( + Composite parent, + Control above, + int middle, + int margin, + ModifyListener lsMod, + String labelKey, + boolean firstOnTab) { + Label wl = new Label(parent, SWT.RIGHT); + wl.setText(BaseMessages.getString(PKG, labelKey)); + PropsUi.setLook(wl); + FormData fdl = new FormData(); + fdl.left = new FormAttachment(0, 0); + fdl.right = new FormAttachment(middle, -margin); + if (firstOnTab) { + fdl.top = new FormAttachment(0, margin); + } else { + fdl.top = new FormAttachment(above, margin); + } + wl.setLayoutData(fdl); + TextVar w = new TextVar(variables, parent, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(w); + w.addModifyListener(lsMod); + FormData fd = new FormData(); + fd.left = new FormAttachment(middle, 0); + if (firstOnTab) { + fd.top = new FormAttachment(0, margin); + } else { + fd.top = new FormAttachment(above, margin); + } + fd.right = new FormAttachment(100, 0); + w.setLayoutData(fd); + return w; + } + + private Control addTextVarRow( + Composite parent, Control above, int middle, int margin, ModifyListener lsMod, String key) { + return addLabeledTextVar( + parent, above, middle, margin, lsMod, "ActionDatabricksJobRun." + key + ".Label"); + } + + private void enableModeFields() { + int idx = wRunMode.getSelectionIndex(); + boolean existing = idx == 0; + boolean submit = idx == 1; + boolean deploy = idx == 2; + wlJobId.setEnabled(existing || deploy); + wJobId.setEnabled(existing || deploy); + wlSubmitJson.setEnabled(submit); + wSubmitJson.setEnabled(submit); + wJobName.setEnabled(deploy); + wUpdateJob.setEnabled(deploy); + wFatJar.setEnabled(deploy); + wbFatJarBrowse.setEnabled(deploy); + wPipeline.setEnabled(deploy); + wbPipelineBrowse.setEnabled(deploy); + wbPipelineOpen.setEnabled(deploy); + wRunConfig.setEnabled(deploy); + wDbfsBase.setEnabled(deploy); + wUploadProjectPackage.setEnabled(deploy); + wProjectHome.setEnabled(deploy); + wProjectPackageFile.setEnabled(deploy); + wbProjectPackageBrowse.setEnabled(deploy); + wEnvironmentConfigFile.setEnabled(deploy); + wbEnvironmentConfigBrowse.setEnabled(deploy); + wClusterId.setEnabled(deploy); + wNewClusterSparkVersion.setEnabled(deploy); + wNewClusterNodeType.setEnabled(deploy); + wNewClusterNumWorkers.setEnabled(deploy); + wNewClusterJson.setEnabled(deploy); + wEnvironmentKey.setEnabled(deploy); + wEnvironmentClient.setEnabled(deploy); + } + + public void getData() { + wName.setText(Const.NVL(action.getName(), "")); + wConnection.setText(Const.NVL(action.getConnectionName(), "")); + if (ActionDatabricksJobRun.MODE_DEPLOY_AND_RUN.equalsIgnoreCase(action.getRunMode())) { + wRunMode.select(2); + } else if (ActionDatabricksJobRun.MODE_SUBMIT_ONCE.equalsIgnoreCase(action.getRunMode())) { + wRunMode.select(1); + } else { + wRunMode.select(0); + } + wJobId.setText(Const.NVL(action.getJobId(), "")); + wJobName.setText(Const.NVL(action.getJobName(), "")); + wUpdateJob.setSelection(action.isUpdateExistingJob()); + wSubmitJson.setText(Const.NVL(action.getSubmitRunJson(), "")); + wFatJar.setText(Const.NVL(action.getFatJarPath(), "")); + wPipeline.setText(Const.NVL(action.getPipelineFilename(), "")); + wRunConfig.setText(Const.NVL(action.getRunConfigurationName(), "")); + wDbfsBase.setText(Const.NVL(action.getDbfsBasePath(), "dbfs:/FileStore/hop")); + wUploadProjectPackage.setSelection(action.isUploadProjectPackage()); + wProjectHome.setText(Const.NVL(action.getProjectHome(), "")); + wProjectPackageFile.setText(Const.NVL(action.getProjectPackageFile(), "")); + wEnvironmentConfigFile.setText(Const.NVL(action.getEnvironmentConfigFile(), "")); + wClusterId.setText(Const.NVL(action.getExistingClusterId(), "")); + // Do not re-inject DEFAULT_* when the user cleared these (Const.NVL treats "" as empty). + // Job create still applies factory defaults for new_cluster / serverless when blank. + wNewClusterSparkVersion.setText(Const.NVL(action.getNewClusterSparkVersion(), "")); + wNewClusterNodeType.setText(Const.NVL(action.getNewClusterNodeTypeId(), "")); + wNewClusterNumWorkers.setText(Const.NVL(action.getNewClusterNumWorkers(), "")); + wNewClusterJson.setText(Const.NVL(action.getNewClusterJson(), "")); + wEnvironmentKey.setText(Const.NVL(action.getEnvironmentKey(), "")); + wEnvironmentClient.setText(Const.NVL(action.getEnvironmentClient(), "")); + if (ActionDatabricksJobRun.WAIT_FIRE_AND_FORGET.equalsIgnoreCase(action.getWaitMode())) { + wWaitMode.select(1); + } else { + wWaitMode.select(0); + } + wTimeout.setText(Const.NVL(action.getTimeoutSeconds(), "3600")); + wPoll.setText(Const.NVL(action.getPollIntervalSeconds(), "15")); + wVarJobId.setText(Const.NVL(action.getResultVariableJobId(), "DatabricksJobId")); + wVarRunId.setText(Const.NVL(action.getResultVariableRunId(), "DatabricksRunId")); + wVarStatus.setText(Const.NVL(action.getResultVariableStatus(), "DatabricksStatus")); + wVarPageUrl.setText(Const.NVL(action.getResultVariablePageUrl(), "DatabricksRunPageUrl")); + wVarError.setText(Const.NVL(action.getResultVariableError(), "DatabricksError")); + } + + private void browseFatJar() { + BaseDialog.presentFileDialog( + shell, + wFatJar, + variables, + new String[] {"*.jar;*.JAR", "*"}, + new String[] { + BaseMessages.getString(PKG, "ActionDatabricksJobRun.FileType.Jar"), + BaseMessages.getString(PKG, "System.FileType.AllFiles") + }, + true); + } + + private void browsePipeline() { + HopPipelineFileType pipelineFileType = new HopPipelineFileType<>(); + BaseDialog.presentFileDialog( + shell, + wPipeline, + variables, + pipelineFileType.getFilterExtensions(), + pipelineFileType.getFilterNames(), + true); + } + + private void browseProjectPackage() { + BaseDialog.presentFileDialog( + shell, + wProjectPackageFile, + variables, + new String[] {"*.zip;*.ZIP", "*"}, + new String[] { + BaseMessages.getString(PKG, "ActionDatabricksJobRun.FileType.Zip"), + BaseMessages.getString(PKG, "System.FileType.AllFiles") + }, + true); + } + + private void browseEnvironmentConfig() { + BaseDialog.presentFileDialog( + shell, + wEnvironmentConfigFile, + variables, + new String[] {"*.json;*.JSON", "*"}, + new String[] { + BaseMessages.getString(PKG, "ActionDatabricksJobRun.FileType.Json"), + BaseMessages.getString(PKG, "System.FileType.AllFiles") + }, + true); + } + + private void openPipeline() { + try { + String filename = variables.resolve(wPipeline.getText()); + if (Utils.isEmpty(filename)) { + return; + } + HopGui.getInstance().fileDelegate.fileOpen(filename); + } catch (Exception e) { + new ErrorDialog( + shell, + BaseMessages.getString(PKG, "ActionDatabricksJobRun.Error.OpenPipeline.Title"), + BaseMessages.getString(PKG, "ActionDatabricksJobRun.Error.OpenPipeline.Message"), + e); + } + } + + private void cancel() { + action.setChanged(changed); + action = null; + dispose(); + } + + private void ok() { + if (Utils.isEmpty(wName.getText())) { + MessageBox mb = new MessageBox(shell, SWT.OK | SWT.ICON_ERROR); + mb.setText(BaseMessages.getString(PKG, "System.TransformActionNameMissing.Title")); + mb.setMessage(BaseMessages.getString(PKG, "System.ActionNameMissing.Msg")); + mb.open(); + return; + } + action.setName(wName.getText()); + action.setConnectionName(wConnection.getText()); + if (wRunMode.getSelectionIndex() == 2) { + action.setRunMode(ActionDatabricksJobRun.MODE_DEPLOY_AND_RUN); + } else if (wRunMode.getSelectionIndex() == 1) { + action.setRunMode(ActionDatabricksJobRun.MODE_SUBMIT_ONCE); + } else { + action.setRunMode(ActionDatabricksJobRun.MODE_RUN_EXISTING); + } + action.setJobId(wJobId.getText()); + action.setSubmitRunJson(wSubmitJson.getText()); + action.setFatJarPath(wFatJar.getText()); + action.setPipelineFilename(wPipeline.getText()); + action.setRunConfigurationName(wRunConfig.getText()); + action.setDbfsBasePath(wDbfsBase.getText()); + action.setUploadProjectPackage(wUploadProjectPackage.getSelection()); + action.setProjectHome(wProjectHome.getText()); + action.setProjectPackageFile(wProjectPackageFile.getText()); + action.setEnvironmentConfigFile(wEnvironmentConfigFile.getText()); + action.setExistingClusterId(wClusterId.getText()); + action.setNewClusterSparkVersion(wNewClusterSparkVersion.getText()); + action.setNewClusterNodeTypeId(wNewClusterNodeType.getText()); + action.setNewClusterNumWorkers(wNewClusterNumWorkers.getText()); + action.setNewClusterJson(wNewClusterJson.getText()); + action.setEnvironmentKey(wEnvironmentKey.getText()); + action.setEnvironmentClient(wEnvironmentClient.getText()); + action.setJobName(wJobName.getText()); + action.setUpdateExistingJob(wUpdateJob.getSelection()); + action.setWaitMode( + wWaitMode.getSelectionIndex() == 1 + ? ActionDatabricksJobRun.WAIT_FIRE_AND_FORGET + : ActionDatabricksJobRun.WAIT_WAIT); + action.setTimeoutSeconds(wTimeout.getText()); + action.setPollIntervalSeconds(wPoll.getText()); + action.setResultVariableJobId(wVarJobId.getText()); + action.setResultVariableRunId(wVarRunId.getText()); + action.setResultVariableStatus(wVarStatus.getText()); + action.setResultVariablePageUrl(wVarPageUrl.getText()); + action.setResultVariableError(wVarError.getText()); + dispose(); + } +} diff --git a/plugins/tech/databricks/src/main/java/org/apache/hop/workflow/actions/databricks/ActionDatabricksJobWait.java b/plugins/tech/databricks/src/main/java/org/apache/hop/workflow/actions/databricks/ActionDatabricksJobWait.java new file mode 100644 index 00000000000..5384e0953d1 --- /dev/null +++ b/plugins/tech/databricks/src/main/java/org/apache/hop/workflow/actions/databricks/ActionDatabricksJobWait.java @@ -0,0 +1,271 @@ +/* + * 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.hop.workflow.actions.databricks; + +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import org.apache.hop.core.Const; +import org.apache.hop.core.ICheckResult; +import org.apache.hop.core.Result; +import org.apache.hop.core.annotations.Action; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.util.Utils; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.databricks.client.DatabricksJobsClient; +import org.apache.hop.databricks.client.DatabricksRunLifeCycleState; +import org.apache.hop.databricks.client.DatabricksRunStatus; +import org.apache.hop.databricks.client.DatabricksRunWaiter; +import org.apache.hop.databricks.client.RestDatabricksJobsClient; +import org.apache.hop.databricks.metadata.DatabricksConnection; +import org.apache.hop.i18n.BaseMessages; +import org.apache.hop.metadata.api.HopMetadataProperty; +import org.apache.hop.metadata.api.IHopMetadataProvider; +import org.apache.hop.workflow.WorkflowMeta; +import org.apache.hop.workflow.action.ActionBase; +import org.apache.hop.workflow.action.IAction; +import org.apache.hop.workflow.action.validator.ActionValidatorUtils; +import org.apache.hop.workflow.action.validator.AndValidator; +import org.apache.hop.workflow.engine.IWorkflowEngine; + +/** + * Poll a Databricks job run until terminal state. Typically used after Job Run in fire-and-forget + * mode, with Run ID defaulting to {@code ${DatabricksRunId}}. + */ +@Action( + id = "DATABRICKS_JOB_WAIT", + name = "i18n::ActionDatabricksJobWait.Name", + description = "i18n::ActionDatabricksJobWait.Description", + image = "databricks-wait.svg", + categoryDescription = "i18n:org.apache.hop.workflow:ActionCategory.Category.BigData", + keywords = "i18n::ActionDatabricksJobWait.keyword", + documentationUrl = "/workflow/actions/databricks-job-wait.html") +@Getter +@Setter +public class ActionDatabricksJobWait extends ActionBase implements Cloneable, IAction { + + private static final Class PKG = ActionDatabricksJobWait.class; + + @HopMetadataProperty(key = "connection") + private String connectionName; + + /** Run id to poll; often {@code ${DatabricksRunId}}. */ + @HopMetadataProperty(key = "run_id") + private String runId = "${DatabricksRunId}"; + + @HopMetadataProperty(key = "timeout_seconds") + private String timeoutSeconds = "3600"; + + @HopMetadataProperty(key = "poll_seconds") + private String pollIntervalSeconds = "15"; + + @HopMetadataProperty(key = "cancel_on_stop") + private boolean cancelOnWorkflowStop = true; + + @HopMetadataProperty(key = "var_job_id") + private String resultVariableJobId = "DatabricksJobId"; + + @HopMetadataProperty(key = "var_run_id") + private String resultVariableRunId = "DatabricksRunId"; + + @HopMetadataProperty(key = "var_status") + private String resultVariableStatus = "DatabricksStatus"; + + @HopMetadataProperty(key = "var_page_url") + private String resultVariablePageUrl = "DatabricksRunPageUrl"; + + @HopMetadataProperty(key = "var_error") + private String resultVariableError = "DatabricksError"; + + private transient ActionDatabricksJobRun.ClientFactory clientFactory = + RestDatabricksJobsClient::create; + + public ActionDatabricksJobWait(String name) { + super(name, ""); + } + + public ActionDatabricksJobWait() { + this(""); + } + + @Override + public Object clone() { + return super.clone(); + } + + public void setClientFactory(ActionDatabricksJobRun.ClientFactory clientFactory) { + this.clientFactory = clientFactory != null ? clientFactory : RestDatabricksJobsClient::create; + } + + @Override + public Result execute(Result previousResult, int nr) { + Result result = previousResult; + result.setResult(false); + result.setNrErrors(1); + setResultVariable(resultVariableError, ""); + + try { + String connName = resolve(connectionName); + if (Utils.isEmpty(connName)) { + throw new HopException( + BaseMessages.getString(PKG, "ActionDatabricksJobWait.Error.NoConnection")); + } + IHopMetadataProvider metadataProvider = getMetadataProvider(); + if (metadataProvider == null) { + throw new HopException( + BaseMessages.getString(PKG, "ActionDatabricksJobWait.Error.NoMetadataProvider")); + } + DatabricksConnection connection = + metadataProvider.getSerializer(DatabricksConnection.class).load(connName); + if (connection == null) { + throw new HopException( + BaseMessages.getString( + PKG, "ActionDatabricksJobWait.Error.ConnectionNotFound", connName)); + } + + String runIdStr = resolve(runId); + if (Utils.isEmpty(runIdStr)) { + throw new HopException( + BaseMessages.getString(PKG, "ActionDatabricksJobWait.Error.NoRunId")); + } + long rid = Long.parseLong(runIdStr.trim()); + + int timeoutSec = Const.toInt(resolve(timeoutSeconds), 3600); + int pollSec = Const.toInt(resolve(pollIntervalSeconds), 15); + + try (DatabricksJobsClient client = clientFactory.create(connection, this)) { + DatabricksRunStatus status = + DatabricksRunWaiter.waitFor( + client, + rid, + timeoutSec, + pollSec, + cancelOnWorkflowStop, + new DatabricksRunWaiter.Hooks() { + @Override + public boolean isStopped() { + return parentWorkflow != null && parentWorkflow.isStopped(); + } + + @Override + public void onStatus(DatabricksRunStatus s) { + applyStatusVariables(s); + } + + @Override + public void logDetailed(String message) { + if (isDetailed()) { + ActionDatabricksJobWait.this.logDetailed(message); + } + } + + @Override + public void logError(String message) { + ActionDatabricksJobWait.this.logError(message); + } + }); + + applyStatusVariables(status); + + if (status.isSuccess()) { + result.setNrErrors(0); + result.setResult(true); + } else if (status.getLifeCycleState() == DatabricksRunLifeCycleState.TERMINATED + && "CANCELED".equalsIgnoreCase(status.getResultState())) { + result.setNrErrors(1); + result.setResult(false); + logError( + BaseMessages.getString( + PKG, + "ActionDatabricksJobWait.Error.Canceled", + Const.NVL(status.getStateMessage(), ""))); + } else { + result.setNrErrors(1); + result.setResult(false); + String msg = + Const.NVL( + status.getStateMessage(), + "Databricks run ended with status " + status.toStatusVariable()); + setResultVariable(resultVariableError, msg); + logError(msg); + } + } + } catch (Exception e) { + result.setNrErrors(1); + result.setResult(false); + setResultVariable( + resultVariableError, e.getMessage() != null ? e.getMessage() : e.toString()); + logError(BaseMessages.getString(PKG, "ActionDatabricksJobWait.Error.Execute"), e); + } + + return result; + } + + private void applyStatusVariables(DatabricksRunStatus status) { + if (status.getJobId() != null) { + setResultVariable(resultVariableJobId, Long.toString(status.getJobId())); + } + setResultVariable(resultVariableRunId, Long.toString(status.getRunId())); + setResultVariable(resultVariableStatus, status.toStatusVariable()); + if (status.getRunPageUrl() != null) { + setResultVariable(resultVariablePageUrl, status.getRunPageUrl()); + } + } + + private void setResultVariable(String name, String value) { + if (Utils.isEmpty(name)) { + return; + } + String varName = resolve(name); + String varValue = value == null ? "" : value; + setVariable(varName, varValue); + IWorkflowEngine parent = getParentWorkflow(); + if (parent != null) { + parent.setVariable(varName, varValue); + IWorkflowEngine p = parent.getParentWorkflow(); + while (p != null) { + p.setVariable(varName, varValue); + p = p.getParentWorkflow(); + } + } + } + + @Override + public boolean isEvaluation() { + return true; + } + + @Override + public boolean isUnconditional() { + return false; + } + + @Override + public void check( + List remarks, + WorkflowMeta workflowMeta, + IVariables variables, + IHopMetadataProvider metadataProvider) { + ActionValidatorUtils.andValidator() + .validate( + this, + "connectionName", + remarks, + AndValidator.putValidators(ActionValidatorUtils.notBlankValidator())); + } +} diff --git a/plugins/tech/databricks/src/main/java/org/apache/hop/workflow/actions/databricks/ActionDatabricksJobWaitDialog.java b/plugins/tech/databricks/src/main/java/org/apache/hop/workflow/actions/databricks/ActionDatabricksJobWaitDialog.java new file mode 100644 index 00000000000..1b63b02ca5e --- /dev/null +++ b/plugins/tech/databricks/src/main/java/org/apache/hop/workflow/actions/databricks/ActionDatabricksJobWaitDialog.java @@ -0,0 +1,242 @@ +/* + * 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.hop.workflow.actions.databricks; + +import org.apache.hop.core.Const; +import org.apache.hop.core.util.Utils; +import org.apache.hop.core.variables.IVariables; +import org.apache.hop.databricks.metadata.DatabricksConnection; +import org.apache.hop.i18n.BaseMessages; +import org.apache.hop.ui.core.PropsUi; +import org.apache.hop.ui.core.dialog.BaseDialog; +import org.apache.hop.ui.core.dialog.MessageBox; +import org.apache.hop.ui.core.widget.MetaSelectionLine; +import org.apache.hop.ui.core.widget.TextVar; +import org.apache.hop.ui.pipeline.transform.BaseTransformDialog; +import org.apache.hop.ui.workflow.action.ActionDialog; +import org.apache.hop.ui.workflow.dialog.WorkflowDialog; +import org.apache.hop.workflow.WorkflowMeta; +import org.apache.hop.workflow.action.IAction; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +/** Dialog for {@link ActionDatabricksJobWait}. */ +public class ActionDatabricksJobWaitDialog extends ActionDialog { + private static final Class PKG = ActionDatabricksJobWait.class; + + private ActionDatabricksJobWait action; + private boolean changed; + + private Text wName; + private MetaSelectionLine wConnection; + private TextVar wRunId; + private TextVar wTimeout; + private TextVar wPoll; + private Button wCancelOnStop; + private TextVar wVarJobId; + private TextVar wVarRunId; + private TextVar wVarStatus; + private TextVar wVarPageUrl; + private TextVar wVarError; + + public ActionDatabricksJobWaitDialog( + Shell parent, + ActionDatabricksJobWait action, + WorkflowMeta workflowMeta, + IVariables variables) { + super(parent, workflowMeta, variables); + this.action = action; + if (this.action.getName() == null) { + this.action.setName(BaseMessages.getString(PKG, "ActionDatabricksJobWait.Name.Default")); + } + } + + @Override + public IAction open() { + shell = new Shell(getParent(), SWT.DIALOG_TRIM | SWT.MIN | SWT.MAX | SWT.RESIZE); + PropsUi.setLook(shell); + WorkflowDialog.setShellImage(shell, action); + + FormLayout formLayout = new FormLayout(); + formLayout.marginWidth = PropsUi.getFormMargin(); + formLayout.marginHeight = PropsUi.getFormMargin(); + shell.setLayout(formLayout); + shell.setText(BaseMessages.getString(PKG, "ActionDatabricksJobWait.Title")); + + int middle = props.getMiddlePct(); + int margin = PropsUi.getMargin(); + changed = action.hasChanged(); + ModifyListener lsMod = e -> action.setChanged(); + + Button wOk = new Button(shell, SWT.PUSH); + wOk.setText(BaseMessages.getString(PKG, "System.Button.OK")); + wOk.addListener(SWT.Selection, e -> ok()); + Button wCancel = new Button(shell, SWT.PUSH); + wCancel.setText(BaseMessages.getString(PKG, "System.Button.Cancel")); + wCancel.addListener(SWT.Selection, e -> cancel()); + BaseTransformDialog.positionBottomButtons(shell, new Button[] {wOk, wCancel}, margin, null); + + Label wlName = new Label(shell, SWT.RIGHT); + wlName.setText(BaseMessages.getString(PKG, "ActionDatabricksJobWait.Name.Label")); + PropsUi.setLook(wlName); + FormData fdlName = new FormData(); + fdlName.left = new FormAttachment(0, 0); + fdlName.right = new FormAttachment(middle, -margin); + fdlName.top = new FormAttachment(0, margin); + wlName.setLayoutData(fdlName); + wName = new Text(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(wName); + wName.addModifyListener(lsMod); + FormData fdName = new FormData(); + fdName.left = new FormAttachment(middle, 0); + fdName.top = new FormAttachment(wlName, 0, SWT.CENTER); + fdName.right = new FormAttachment(100, 0); + wName.setLayoutData(fdName); + + wConnection = + new MetaSelectionLine<>( + variables, + metadataProvider, + DatabricksConnection.class, + shell, + SWT.NONE, + BaseMessages.getString(PKG, "ActionDatabricksJobWait.Connection.Label"), + BaseMessages.getString(PKG, "ActionDatabricksJobWait.Connection.Tooltip")); + FormData fdConn = new FormData(); + fdConn.left = new FormAttachment(0, 0); + fdConn.top = new FormAttachment(wName, margin); + fdConn.right = new FormAttachment(100, 0); + wConnection.setLayoutData(fdConn); + try { + wConnection.fillItems(); + } catch (Exception ignored) { + // empty metadata store + } + wConnection.addModifyListener(lsMod); + + Control last = wConnection; + wRunId = addField(shell, last, middle, margin, lsMod, "RunId"); + last = wRunId; + wTimeout = addField(shell, last, middle, margin, lsMod, "Timeout"); + last = wTimeout; + wPoll = addField(shell, last, middle, margin, lsMod, "Poll"); + last = wPoll; + + wCancelOnStop = new Button(shell, SWT.CHECK); + PropsUi.setLook(wCancelOnStop); + wCancelOnStop.setText( + BaseMessages.getString(PKG, "ActionDatabricksJobWait.CancelOnStop.Label")); + FormData fdCancel = new FormData(); + fdCancel.left = new FormAttachment(middle, 0); + fdCancel.top = new FormAttachment(last, margin); + wCancelOnStop.setLayoutData(fdCancel); + wCancelOnStop.addListener(SWT.Selection, e -> action.setChanged()); + last = wCancelOnStop; + + wVarJobId = addField(shell, last, middle, margin, lsMod, "VarJobId"); + last = wVarJobId; + wVarRunId = addField(shell, last, middle, margin, lsMod, "VarRunId"); + last = wVarRunId; + wVarStatus = addField(shell, last, middle, margin, lsMod, "VarStatus"); + last = wVarStatus; + wVarPageUrl = addField(shell, last, middle, margin, lsMod, "VarPageUrl"); + last = wVarPageUrl; + wVarError = addField(shell, last, middle, margin, lsMod, "VarError"); + + getData(); + wName.setFocus(); + BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel()); + return action; + } + + private TextVar addField( + org.eclipse.swt.widgets.Composite parent, + Control above, + int middle, + int margin, + ModifyListener lsMod, + String key) { + Label wl = new Label(parent, SWT.RIGHT); + wl.setText(BaseMessages.getString(PKG, "ActionDatabricksJobWait." + key + ".Label")); + PropsUi.setLook(wl); + FormData fdl = new FormData(); + fdl.left = new FormAttachment(0, 0); + fdl.right = new FormAttachment(middle, -margin); + fdl.top = new FormAttachment(above, margin); + wl.setLayoutData(fdl); + TextVar w = new TextVar(variables, parent, SWT.SINGLE | SWT.LEFT | SWT.BORDER); + PropsUi.setLook(w); + w.addModifyListener(lsMod); + FormData fd = new FormData(); + fd.left = new FormAttachment(middle, 0); + fd.top = new FormAttachment(above, margin); + fd.right = new FormAttachment(100, 0); + w.setLayoutData(fd); + return w; + } + + private void getData() { + wName.setText(Const.NVL(action.getName(), "")); + wConnection.setText(Const.NVL(action.getConnectionName(), "")); + wRunId.setText(Const.NVL(action.getRunId(), "${DatabricksRunId}")); + wTimeout.setText(Const.NVL(action.getTimeoutSeconds(), "3600")); + wPoll.setText(Const.NVL(action.getPollIntervalSeconds(), "15")); + wCancelOnStop.setSelection(action.isCancelOnWorkflowStop()); + wVarJobId.setText(Const.NVL(action.getResultVariableJobId(), "DatabricksJobId")); + wVarRunId.setText(Const.NVL(action.getResultVariableRunId(), "DatabricksRunId")); + wVarStatus.setText(Const.NVL(action.getResultVariableStatus(), "DatabricksStatus")); + wVarPageUrl.setText(Const.NVL(action.getResultVariablePageUrl(), "DatabricksRunPageUrl")); + wVarError.setText(Const.NVL(action.getResultVariableError(), "DatabricksError")); + } + + private void cancel() { + action.setChanged(changed); + action = null; + dispose(); + } + + private void ok() { + if (Utils.isEmpty(wName.getText())) { + MessageBox mb = new MessageBox(shell, SWT.OK | SWT.ICON_ERROR); + mb.setText(BaseMessages.getString(PKG, "System.TransformActionNameMissing.Title")); + mb.setMessage(BaseMessages.getString(PKG, "System.ActionNameMissing.Msg")); + mb.open(); + return; + } + action.setName(wName.getText()); + action.setConnectionName(wConnection.getText()); + action.setRunId(wRunId.getText()); + action.setTimeoutSeconds(wTimeout.getText()); + action.setPollIntervalSeconds(wPoll.getText()); + action.setCancelOnWorkflowStop(wCancelOnStop.getSelection()); + action.setResultVariableJobId(wVarJobId.getText()); + action.setResultVariableRunId(wVarRunId.getText()); + action.setResultVariableStatus(wVarStatus.getText()); + action.setResultVariablePageUrl(wVarPageUrl.getText()); + action.setResultVariableError(wVarError.getText()); + dispose(); + } +} diff --git a/plugins/tech/databricks/src/main/resources/databricks-connection.svg b/plugins/tech/databricks/src/main/resources/databricks-connection.svg new file mode 100644 index 00000000000..e9c86c7a1dc --- /dev/null +++ b/plugins/tech/databricks/src/main/resources/databricks-connection.svg @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/plugins/tech/databricks/src/main/resources/databricks-job-run.svg b/plugins/tech/databricks/src/main/resources/databricks-job-run.svg new file mode 100644 index 00000000000..5b74b5c3f70 --- /dev/null +++ b/plugins/tech/databricks/src/main/resources/databricks-job-run.svg @@ -0,0 +1,21 @@ + + + + + + diff --git a/plugins/tech/databricks/src/main/resources/databricks-job-wait.svg b/plugins/tech/databricks/src/main/resources/databricks-job-wait.svg new file mode 100644 index 00000000000..fcf3d78b8c3 --- /dev/null +++ b/plugins/tech/databricks/src/main/resources/databricks-job-wait.svg @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/plugins/tech/databricks/src/main/resources/databricks-logo.svg b/plugins/tech/databricks/src/main/resources/databricks-logo.svg new file mode 100644 index 00000000000..4928c857888 --- /dev/null +++ b/plugins/tech/databricks/src/main/resources/databricks-logo.svg @@ -0,0 +1,11 @@ + + + + diff --git a/plugins/tech/databricks/src/main/resources/databricks-run.svg b/plugins/tech/databricks/src/main/resources/databricks-run.svg new file mode 100644 index 00000000000..223ee231656 --- /dev/null +++ b/plugins/tech/databricks/src/main/resources/databricks-run.svg @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/plugins/tech/databricks/src/main/resources/databricks-wait.svg b/plugins/tech/databricks/src/main/resources/databricks-wait.svg new file mode 100644 index 00000000000..e20d0f17d15 --- /dev/null +++ b/plugins/tech/databricks/src/main/resources/databricks-wait.svg @@ -0,0 +1,34 @@ + + + + + + + diff --git a/plugins/tech/databricks/src/main/resources/org/apache/hop/databricks/metadata/messages/messages_en_US.properties b/plugins/tech/databricks/src/main/resources/org/apache/hop/databricks/metadata/messages/messages_en_US.properties new file mode 100644 index 00000000000..e93f4fc2496 --- /dev/null +++ b/plugins/tech/databricks/src/main/resources/org/apache/hop/databricks/metadata/messages/messages_en_US.properties @@ -0,0 +1,33 @@ +# +# 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. +# + +DatabricksConnection.Name=Databricks Connection +DatabricksConnection.Description=Workspace connection for Databricks Jobs API (PAT) +DatabricksConnection.Name.Label=Name +DatabricksConnection.Description.Label=Description +DatabricksConnection.Description.Tooltip=Optional free-text description +DatabricksConnection.Host.Label=Workspace host +DatabricksConnection.Host.Tooltip=Workspace URL host, e.g. https://adb-1234567890123456.7.azuredatabricks.net (scheme optional) +DatabricksConnection.Token.Label=Personal access token +DatabricksConnection.Token.Tooltip=Databricks PAT (password field). Prefer variables for secrets. Never logged. +DatabricksConnection.ApiBase.Label=API base path +DatabricksConnection.ApiBase.Tooltip=Jobs API prefix (default /api/2.1) +DatabricksConnection.Test.Label=Test connection +DatabricksConnection.Test.Success.Title=Connection OK +DatabricksConnection.Test.Success.Message=Connected to Databricks workspace as: {0} +DatabricksConnection.Test.Error.Title=Connection failed +DatabricksConnection.Test.Error.Message=Unable to connect to the Databricks workspace diff --git a/plugins/tech/databricks/src/main/resources/org/apache/hop/workflow/actions/databricks/messages/messages_en_US.properties b/plugins/tech/databricks/src/main/resources/org/apache/hop/workflow/actions/databricks/messages/messages_en_US.properties new file mode 100644 index 00000000000..b9736d75aef --- /dev/null +++ b/plugins/tech/databricks/src/main/resources/org/apache/hop/workflow/actions/databricks/messages/messages_en_US.properties @@ -0,0 +1,115 @@ +# +# 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. +# + +ActionDatabricksJobRun.Name=Databricks Job Run +ActionDatabricksJobRun.Description=Run or submit a Databricks job and optionally wait for completion +ActionDatabricksJobRun.Name.Default=Databricks Job Run +ActionDatabricksJobRun.Title=Databricks Job Run +ActionDatabricksJobRun.keyword=databricks,job,spark,cluster,big data +ActionDatabricksJobRun.Name.Label=Action name +ActionDatabricksJobRun.Tab.Job=Job +ActionDatabricksJobRun.Tab.Deploy=Deploy +ActionDatabricksJobRun.Tab.Package=Package +ActionDatabricksJobRun.Tab.Compute=Compute +ActionDatabricksJobRun.Tab.Run=Run +ActionDatabricksJobRun.Connection.Label=Databricks connection +ActionDatabricksJobRun.Connection.Tooltip=Workspace connection (Jobs API / PAT) +ActionDatabricksJobRun.RunMode.Label=Mode +ActionDatabricksJobRun.RunMode.RunExisting=Run existing job +ActionDatabricksJobRun.RunMode.SubmitOnce=One-time submit (runs/submit JSON) +ActionDatabricksJobRun.RunMode.DeployAndRun=Deploy & run (upload + MainSpark JAR job) +ActionDatabricksJobRun.JobId.Label=Job ID +ActionDatabricksJobRun.SubmitJson.Label=Submit JSON +ActionDatabricksJobRun.FatJar.Label=Fat jar (local path) +ActionDatabricksJobRun.Pipeline.Label=Pipeline file (.hpl) +ActionDatabricksJobRun.RunConfig.Label=Pipeline run configuration +ActionDatabricksJobRun.RunConfig.Tooltip=Native Spark (or other) pipeline run configuration name exported with metadata at deploy time +ActionDatabricksJobRun.FileType.Jar=Java archive (*.jar) +ActionDatabricksJobRun.FileType.Zip=Zip archive (*.zip) +ActionDatabricksJobRun.FileType.Json=JSON (*.json) +ActionDatabricksJobRun.Error.OpenPipeline.Title=Error opening pipeline +ActionDatabricksJobRun.Error.OpenPipeline.Message=Unable to open the selected pipeline file +ActionDatabricksJobRun.DbfsBase.Label=Upload base directory (Volume or DBFS) +ActionDatabricksJobRun.UploadProjectPackage.Label=Upload Spark project package +ActionDatabricksJobRun.UploadProjectPackage.Tooltip=Export and upload a Native Spark project package (same layout as Tools \u2192 Export project package for Native Spark) so nested Simple Mapping / Pipeline Executor files under PROJECT_HOME resolve on the cluster. Uses MainSpark --HopProjectPackage. +ActionDatabricksJobRun.ProjectHome.Label=Project home (export) +ActionDatabricksJobRun.ProjectHome.Tooltip=Project home directory used when exporting a package. Leave empty to use PROJECT_HOME. Ignored when Project package file is set. +ActionDatabricksJobRun.ProjectPackageFile.Label=Project package file (optional) +ActionDatabricksJobRun.ProjectPackageFile.Tooltip=Optional existing hop-spark project package zip. When blank and Upload Spark project package is checked, Hop exports from Project home / PROJECT_HOME at deploy time. +ActionDatabricksJobRun.EnvironmentConfigFile.Label=Environment config file (optional) +ActionDatabricksJobRun.EnvironmentConfigFile.Tooltip=Optional described-variables JSON uploaded as env-config.json and passed to MainSpark as --HopConfigFile (same as MainSpark 4th argument / named form). +ActionDatabricksJobRun.ClusterId.Label=Cluster / compute +ActionDatabricksJobRun.ClusterId.Tooltip=Deploy & run compute (pick one): (1) Classic existing cluster — paste the all-purpose cluster ID from the workspace. (2) Classic job cluster — enter the literal value new_cluster and set spark_version / node_type_id / num_workers below (or optional new_cluster JSON); Databricks starts a cluster for the run and terminates it afterward. (3) Serverless — enter the literal value serverless; no cluster fields are sent; uses environment_key + client below and java_dependencies for the jar. +ActionDatabricksJobRun.NewClusterSparkVersion.Label=new_cluster spark_version +ActionDatabricksJobRun.NewClusterSparkVersion.Tooltip=Databricks Runtime spark_version for a classic job cluster, e.g. 18.2.x-scala2.13. Used when Cluster / compute is new_cluster. +ActionDatabricksJobRun.NewClusterNodeType.Label=new_cluster node_type_id +ActionDatabricksJobRun.NewClusterNodeType.Tooltip=Cloud node type for a classic job cluster. On AWS prefer i3.xlarge (local NVMe; no extra EBS). m5.xlarge often requires EBS volumes. Azure example: Standard_DS3_v2. Used when Cluster / compute is new_cluster. +ActionDatabricksJobRun.NewClusterNumWorkers.Label=new_cluster num_workers +ActionDatabricksJobRun.NewClusterNumWorkers.Tooltip=Worker count for a classic job cluster. Used when Cluster / compute is new_cluster. +ActionDatabricksJobRun.NewClusterJson.Label=new_cluster JSON (optional) +ActionDatabricksJobRun.NewClusterJson.Tooltip=Optional full new_cluster JSON object. When set and Cluster / compute is new_cluster, overrides spark_version / node_type_id / num_workers. +ActionDatabricksJobRun.EnvironmentKey.Label=Serverless environment_key +ActionDatabricksJobRun.EnvironmentKey.Tooltip=Job environment key for serverless compute (default: default). Used when Cluster / compute is serverless. Task references this via environment_key; no existing_cluster_id or new_cluster is sent. +ActionDatabricksJobRun.EnvironmentClient.Label=Serverless environment client +ActionDatabricksJobRun.EnvironmentClient.Tooltip=Serverless environment spec.client version, e.g. 4 for Spark 4.x. Used when Cluster / compute is serverless. +ActionDatabricksJobRun.JobName.Label=Job name (create) +ActionDatabricksJobRun.UpdateJob.Label=Update existing job (use Job ID above) +ActionDatabricksJobRun.Error.NoClusterId=Cluster / compute is required for Deploy & run (existing cluster id, new_cluster, or serverless) +ActionDatabricksJobRun.WaitMode.Label=Wait mode +ActionDatabricksJobRun.WaitMode.Wait=Wait for completion +ActionDatabricksJobRun.WaitMode.FireAndForget=Fire and forget +ActionDatabricksJobRun.ReferencedObject.Description=Pipeline +ActionDatabricksJobRun.Exception.MetaDataLoad=Unexpected error during pipeline metadata load +ActionDatabricksJobRun.Timeout.Label=Timeout (seconds, 0=none) +ActionDatabricksJobRun.Poll.Label=Poll interval (seconds) +ActionDatabricksJobRun.VarJobId.Label=Result variable: Job ID +ActionDatabricksJobRun.VarRunId.Label=Result variable: Run ID +ActionDatabricksJobRun.VarStatus.Label=Result variable: Status +ActionDatabricksJobRun.VarPageUrl.Label=Result variable: Run page URL +ActionDatabricksJobRun.VarError.Label=Result variable: Error +ActionDatabricksJobRun.Error.NoConnection=Please select a Databricks connection +ActionDatabricksJobRun.Error.NoMetadataProvider=No metadata provider available +ActionDatabricksJobRun.Error.ConnectionNotFound=Databricks connection not found: {0} +ActionDatabricksJobRun.Error.NoJobId=Job ID is required when running an existing job +ActionDatabricksJobRun.Error.NoSubmitJson=Submit JSON is required for one-time submit mode +ActionDatabricksJobRun.Error.WorkflowStopped=Workflow was stopped while waiting for Databricks run +ActionDatabricksJobRun.Error.Timeout=Timed out after {0} seconds waiting for Databricks run +ActionDatabricksJobRun.Error.Execute=Error executing Databricks Job Run + +ActionDatabricksJobWait.Name=Databricks Job Wait +ActionDatabricksJobWait.Description=Poll a Databricks job run until it completes (companion to fire-and-forget Job Run) +ActionDatabricksJobWait.Name.Default=Databricks Job Wait +ActionDatabricksJobWait.Title=Databricks Job Wait +ActionDatabricksJobWait.keyword=databricks,job,wait,poll,status +ActionDatabricksJobWait.Name.Label=Action name +ActionDatabricksJobWait.Connection.Label=Databricks connection +ActionDatabricksJobWait.Connection.Tooltip=Workspace connection (Jobs API / PAT) +ActionDatabricksJobWait.RunId.Label=Run ID +ActionDatabricksJobWait.Timeout.Label=Timeout (seconds, 0=none) +ActionDatabricksJobWait.Poll.Label=Poll interval (seconds) +ActionDatabricksJobWait.CancelOnStop.Label=Cancel Databricks run if workflow is stopped +ActionDatabricksJobWait.VarJobId.Label=Result variable: Job ID +ActionDatabricksJobWait.VarRunId.Label=Result variable: Run ID +ActionDatabricksJobWait.VarStatus.Label=Result variable: Status +ActionDatabricksJobWait.VarPageUrl.Label=Result variable: Run page URL +ActionDatabricksJobWait.VarError.Label=Result variable: Error +ActionDatabricksJobWait.Error.NoConnection=Please select a Databricks connection +ActionDatabricksJobWait.Error.NoMetadataProvider=No metadata provider available +ActionDatabricksJobWait.Error.ConnectionNotFound=Databricks connection not found: {0} +ActionDatabricksJobWait.Error.NoRunId=Run ID is required (e.g. variable DatabricksRunId from Job Run) +ActionDatabricksJobWait.Error.Canceled=Databricks run was canceled: {0} +ActionDatabricksJobWait.Error.Execute=Error waiting for Databricks run diff --git a/plugins/tech/databricks/src/test/java/org/apache/hop/databricks/client/DatabricksRunWaiterTest.java b/plugins/tech/databricks/src/test/java/org/apache/hop/databricks/client/DatabricksRunWaiterTest.java new file mode 100644 index 00000000000..c2ee00c4fd4 --- /dev/null +++ b/plugins/tech/databricks/src/test/java/org/apache/hop/databricks/client/DatabricksRunWaiterTest.java @@ -0,0 +1,107 @@ +/* + * 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.hop.databricks.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.hop.core.exception.HopException; +import org.junit.jupiter.api.Test; + +class DatabricksRunWaiterTest { + + @Test + void pollsUntilTerminalSuccess() throws Exception { + DatabricksJobsClient client = mock(DatabricksJobsClient.class); + when(client.getRun(9L)) + .thenReturn( + new DatabricksRunStatus(9L, 1L, DatabricksRunLifeCycleState.RUNNING, null, null, null)) + .thenReturn( + new DatabricksRunStatus( + 9L, 1L, DatabricksRunLifeCycleState.TERMINATED, "SUCCESS", "ok", null)); + + int[] polls = {0}; + DatabricksRunStatus status = + DatabricksRunWaiter.waitFor( + client, + 9L, + 60, + 1, + true, + new DatabricksRunWaiter.Hooks() { + @Override + public boolean isStopped() { + return false; + } + + @Override + public void onStatus(DatabricksRunStatus s) { + polls[0]++; + } + + @Override + public void logDetailed(String message) {} + + @Override + public void logError(String message) {} + }); + + assertTrue(status.isSuccess()); + assertEquals(2, polls[0]); + verify(client, times(2)).getRun(9L); + } + + @Test + void timeoutFails() throws Exception { + DatabricksJobsClient client = mock(DatabricksJobsClient.class); + when(client.getRun(1L)) + .thenReturn( + new DatabricksRunStatus( + 1L, null, DatabricksRunLifeCycleState.RUNNING, null, null, null)); + + assertThrows( + HopException.class, + () -> + DatabricksRunWaiter.waitFor( + client, + 1L, + 1, + 1, + false, + new DatabricksRunWaiter.Hooks() { + @Override + public boolean isStopped() { + return false; + } + + @Override + public void onStatus(DatabricksRunStatus status) {} + + @Override + public void logDetailed(String message) {} + + @Override + public void logError(String message) {} + })); + } +} diff --git a/plugins/tech/databricks/src/test/java/org/apache/hop/databricks/client/RestDatabricksJobsClientTest.java b/plugins/tech/databricks/src/test/java/org/apache/hop/databricks/client/RestDatabricksJobsClientTest.java new file mode 100644 index 00000000000..351a352a37b --- /dev/null +++ b/plugins/tech/databricks/src/test/java/org/apache/hop/databricks/client/RestDatabricksJobsClientTest.java @@ -0,0 +1,381 @@ +/* + * 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.hop.databricks.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import java.net.http.HttpClient; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Optional; +import org.apache.hop.core.Const; +import org.apache.hop.core.encryption.Encr; +import org.apache.hop.core.encryption.TwoWayPasswordEncoderPluginType; +import org.apache.hop.core.exception.HopException; +import org.apache.hop.core.plugins.PluginRegistry; +import org.apache.hop.core.util.EnvUtil; +import org.apache.hop.core.variables.Variables; +import org.apache.hop.databricks.metadata.DatabricksConnection; +import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class RestDatabricksJobsClientTest { + + @RegisterExtension + static RestoreHopEngineEnvironmentExtension env = new RestoreHopEngineEnvironmentExtension(); + + private WireMockServer server; + private RestDatabricksJobsClient client; + + @BeforeAll + static void setUpClass() throws HopException { + PluginRegistry.addPluginType(TwoWayPasswordEncoderPluginType.getInstance()); + PluginRegistry.init(); + String passwordEncoderPluginID = + Const.NVL(EnvUtil.getSystemProperty(Const.HOP_PASSWORD_ENCODER_PLUGIN), "Hop"); + Encr.init(passwordEncoderPluginID); + } + + @BeforeEach + void setUp() { + server = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort()); + server.start(); + WireMock.configureFor("localhost", server.port()); + client = + RestDatabricksJobsClient.createForTest( + "http://localhost:" + server.port(), + "/api/2.1", + "test-token", + HttpClient.newHttpClient()); + } + + @AfterEach + void tearDown() { + if (server != null) { + server.stop(); + } + } + + @Test + void normalizeHostAddsHttps() { + assertEquals( + "https://my.databricks.net", RestDatabricksJobsClient.normalizeHost("my.databricks.net")); + assertEquals( + "https://my.databricks.net", + RestDatabricksJobsClient.normalizeHost("https://my.databricks.net/")); + } + + @Test + void createResolvesHostAndDecryptsTokenVariable() throws Exception { + String plainToken = "dapi-secret-token"; + Variables variables = new Variables(); + variables.setVariable("DBX_HOST", "http://localhost:" + server.port()); + variables.setVariable("DBX_TOKEN", Encr.encryptPasswordIfNotUsingVariables(plainToken)); + + DatabricksConnection connection = new DatabricksConnection(); + connection.setHost("${DBX_HOST}"); + connection.setToken("${DBX_TOKEN}"); + connection.setApiBasePath("/api/2.1"); + + server.stubFor( + WireMock.get(WireMock.urlEqualTo("/api/2.0/preview/scim/v2/Me")) + .withHeader("Authorization", WireMock.equalTo("Bearer " + plainToken)) + .willReturn( + WireMock.aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"userName\":\"bob@example.com\"}"))); + + try (RestDatabricksJobsClient resolved = + RestDatabricksJobsClient.create(connection, variables)) { + assertEquals("bob@example.com", resolved.testConnection()); + } + } + + @Test + void createDecryptsEncryptedLiteralToken() throws Exception { + String plainToken = "literal-pat"; + DatabricksConnection connection = new DatabricksConnection(); + connection.setHost("http://localhost:" + server.port()); + connection.setToken(Encr.encryptPasswordIfNotUsingVariables(plainToken)); + connection.setApiBasePath("/api/2.1"); + + server.stubFor( + WireMock.get(WireMock.urlEqualTo("/api/2.0/preview/scim/v2/Me")) + .withHeader("Authorization", WireMock.equalTo("Bearer " + plainToken)) + .willReturn( + WireMock.aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"userName\":\"carol@example.com\"}"))); + + try (RestDatabricksJobsClient resolved = + RestDatabricksJobsClient.create(connection, new Variables())) { + assertEquals("carol@example.com", resolved.testConnection()); + } + } + + @Test + void sanitizeErrorRedactsBearer() { + String s = RestDatabricksJobsClient.sanitizeError("error bearer abc.def.ghi more"); + assertTrue(s.contains("***")); + assertFalse(s.contains("abc.def.ghi")); + } + + @Test + void testConnectionUsesScimMe() throws Exception { + server.stubFor( + WireMock.get(WireMock.urlEqualTo("/api/2.0/preview/scim/v2/Me")) + .withHeader("Authorization", WireMock.equalTo("Bearer test-token")) + .willReturn( + WireMock.aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"userName\":\"alice@example.com\"}"))); + + assertEquals("alice@example.com", client.testConnection()); + } + + @Test + void runNowReturnsRunId() throws Exception { + server.stubFor( + WireMock.post(WireMock.urlEqualTo("/api/2.1/jobs/run-now")) + .willReturn( + WireMock.aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"run_id\": 99, \"number_in_job\": 1}"))); + + long runId = client.runNow(42L, Map.of("k", "v")); + assertEquals(99L, runId); + } + + @Test + void getRunParsesState() throws Exception { + server.stubFor( + WireMock.get(WireMock.urlPathEqualTo("/api/2.1/jobs/runs/get")) + .withQueryParam("run_id", WireMock.equalTo("7")) + .willReturn( + WireMock.aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + """ + { + "run_id": 7, + "job_id": 3, + "run_page_url": "https://example/runs/7", + "state": { + "life_cycle_state": "TERMINATED", + "result_state": "SUCCESS", + "state_message": "ok" + } + } + """))); + + DatabricksRunStatus status = client.getRun(7L); + assertEquals(7L, status.getRunId()); + assertEquals(3L, status.getJobId()); + assertTrue(status.isSuccess()); + assertEquals("SUCCESS", status.toStatusVariable()); + assertEquals("https://example/runs/7", status.getRunPageUrl()); + } + + @Test + void httpErrorSurfacesStatus() { + server.stubFor( + WireMock.get(WireMock.urlEqualTo("/api/2.0/preview/scim/v2/Me")) + .willReturn( + WireMock.aResponse().withStatus(401).withBody("{\"error\":\"unauthorized\"}"))); + server.stubFor( + WireMock.get(WireMock.urlPathEqualTo("/api/2.1/jobs/list")) + .willReturn( + WireMock.aResponse().withStatus(401).withBody("{\"error\":\"unauthorized\"}"))); + + HopException ex = assertThrows(HopException.class, () -> client.testConnection()); + assertTrue(ex.getMessage().contains("401")); + } + + @Test + void createJobReturnsJobId() throws Exception { + server.stubFor( + WireMock.post(WireMock.urlEqualTo("/api/2.1/jobs/create")) + .willReturn( + WireMock.aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"job_id\": 55}"))); + assertEquals(55L, client.createJob("{\"name\":\"n\",\"tasks\":[]}")); + } + + @Test + void lifeCycleTerminalHelpers() { + assertTrue(DatabricksRunLifeCycleState.TERMINATED.isTerminal()); + assertFalse(DatabricksRunLifeCycleState.RUNNING.isTerminal()); + assertEquals( + DatabricksRunLifeCycleState.PENDING, DatabricksRunLifeCycleState.fromApi("pending")); + } + + @Test + void isFilesApiPathDetectsVolumesAndWorkspace() { + assertTrue(RestDatabricksJobsClient.isFilesApiPath("/Volumes/c/s/v/file.jar")); + assertTrue(RestDatabricksJobsClient.isFilesApiPath("/Workspace/Users/a@b.com/x")); + assertFalse(RestDatabricksJobsClient.isFilesApiPath("/FileStore/hop/x.jar")); + assertFalse(RestDatabricksJobsClient.isFilesApiPath("/tmp/x")); + } + + @Test + void normalizeDbfsPathStripsSchemeAndSelectsVolumes() throws Exception { + assertEquals( + "/Volumes/c/s/v/j.jar", + RestDatabricksJobsClient.normalizeDbfsPath("dbfs:/Volumes/c/s/v/j.jar")); + assertEquals( + "/FileStore/hop/j.jar", + RestDatabricksJobsClient.normalizeDbfsPath("dbfs:/FileStore/hop/j.jar")); + assertTrue( + RestDatabricksJobsClient.isFilesApiPath( + RestDatabricksJobsClient.normalizeDbfsPath("/Volumes/c/s/v/j.jar"))); + } + + @Test + void encodeFilesApiPathEncodesSegments() { + assertEquals( + "/Volumes/apache-hop/default/jars/hop-native.jar", + RestDatabricksJobsClient.encodeFilesApiPath( + "/Volumes/apache-hop/default/jars/hop-native.jar")); + assertEquals( + "/Volumes/c/s/v/my%20file.jar", + RestDatabricksJobsClient.encodeFilesApiPath("/Volumes/c/s/v/my file.jar")); + } + + @Test + void uploadVolumePathUsesFilesApiPut() throws Exception { + Path temp = Files.createTempFile("hop-dbx-upload-", ".bin"); + try { + byte[] payload = "hop-volume-upload".getBytes(StandardCharsets.UTF_8); + Files.write(temp, payload); + + server.stubFor( + WireMock.put( + WireMock.urlEqualTo( + "/api/2.0/fs/files/Volumes/apache-hop/default/jars/hop-native.jar?overwrite=true")) + .withHeader("Authorization", WireMock.equalTo("Bearer test-token")) + .withHeader("Content-Type", WireMock.equalTo("application/octet-stream")) + .withRequestBody(WireMock.binaryEqualTo(payload)) + .willReturn(WireMock.aResponse().withStatus(204))); + + client.uploadToDbfs(temp, "/Volumes/apache-hop/default/jars/hop-native.jar"); + + server.verify( + WireMock.putRequestedFor( + WireMock.urlEqualTo( + "/api/2.0/fs/files/Volumes/apache-hop/default/jars/hop-native.jar?overwrite=true"))); + server.verify(0, WireMock.postRequestedFor(WireMock.urlEqualTo("/api/2.0/dbfs/create"))); + } finally { + Files.deleteIfExists(temp); + } + } + + @Test + void getFileMetadataFilesApiUsesHeadContentLength() throws Exception { + server.stubFor( + WireMock.head( + WireMock.urlEqualTo( + "/api/2.0/fs/files/Volumes/apache-hop/default/jars/hop-native.jar")) + .withHeader("Authorization", WireMock.equalTo("Bearer test-token")) + .willReturn( + WireMock.aResponse().withStatus(200).withHeader("Content-Length", "84200123"))); + + WorkspaceFileMetadata meta = + client.getFileMetadata("/Volumes/apache-hop/default/jars/hop-native.jar"); + assertTrue(meta.exists()); + assertEquals(84200123L, meta.sizeBytes()); + } + + @Test + void getFileMetadataMissingReturnsNotExists() throws Exception { + server.stubFor( + WireMock.head(WireMock.urlPathMatching("/api/2.0/fs/files/Volumes/.*missing.jar")) + .willReturn(WireMock.aResponse().withStatus(404))); + server.stubFor( + WireMock.get(WireMock.urlPathMatching("/api/2.0/fs/files/Volumes/.*missing.jar")) + .willReturn(WireMock.aResponse().withStatus(404))); + + WorkspaceFileMetadata meta = client.getFileMetadata("/Volumes/c/s/v/missing.jar"); + assertFalse(meta.exists()); + } + + @Test + void downloadTextIfExistsReturnsSidecar() throws Exception { + String hash = "a".repeat(64); + server.stubFor( + WireMock.get( + WireMock.urlEqualTo( + "/api/2.0/fs/files/Volumes/apache-hop/default/jars/hop-native.jar.sha256")) + .willReturn( + WireMock.aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/plain") + .withBody(hash + "\n"))); + + assertEquals( + Optional.of(hash + "\n"), + client.downloadTextIfExists("/Volumes/apache-hop/default/jars/hop-native.jar.sha256")); + } + + @Test + void uploadFileStorePathUsesDbfsApi() throws Exception { + Path temp = Files.createTempFile("hop-dbx-upload-dbfs-", ".bin"); + try { + Files.writeString(temp, "x"); + + server.stubFor( + WireMock.post(WireMock.urlEqualTo("/api/2.0/dbfs/create")) + .willReturn( + WireMock.aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"handle\": 1}"))); + server.stubFor( + WireMock.post(WireMock.urlEqualTo("/api/2.0/dbfs/add-block")) + .willReturn(WireMock.aResponse().withStatus(200).withBody("{}"))); + server.stubFor( + WireMock.post(WireMock.urlEqualTo("/api/2.0/dbfs/close")) + .willReturn(WireMock.aResponse().withStatus(200).withBody("{}"))); + + client.uploadToDbfs(temp, "dbfs:/FileStore/hop/hop-native.jar"); + + server.verify(WireMock.postRequestedFor(WireMock.urlEqualTo("/api/2.0/dbfs/create"))); + server.verify(0, WireMock.putRequestedFor(WireMock.urlPathMatching("/api/2.0/fs/files/.*"))); + } finally { + Files.deleteIfExists(temp); + } + } +} diff --git a/plugins/tech/databricks/src/test/java/org/apache/hop/databricks/deploy/DatabricksJobSpecFactoryTest.java b/plugins/tech/databricks/src/test/java/org/apache/hop/databricks/deploy/DatabricksJobSpecFactoryTest.java new file mode 100644 index 00000000000..7e5f8aa7c9a --- /dev/null +++ b/plugins/tech/databricks/src/test/java/org/apache/hop/databricks/deploy/DatabricksJobSpecFactoryTest.java @@ -0,0 +1,232 @@ +/* + * 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.hop.databricks.deploy; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.hop.core.exception.HopException; +import org.junit.jupiter.api.Test; + +class DatabricksJobSpecFactoryTest { + + @Test + void createJobJsonContainsMainSparkAndPaths() throws Exception { + String json = + DatabricksJobSpecFactory.buildCreateJobJson( + "hop-job", + "cluster-1", + "/FileStore/hop/j.jar", + "/FileStore/hop/p.hpl", + "/FileStore/hop/m.json", + "spark-local"); + assertTrue(json.contains("hop-job"), json); + assertTrue(json.contains("cluster-1"), json); + assertTrue(json.contains("existing_cluster_id"), json); + assertTrue(json.contains("MainSpark"), json); + assertTrue(json.contains("j.jar"), json); + assertTrue(json.contains("p.hpl"), json); + assertTrue(json.contains("spark-local"), json); + assertFalse(json.contains("environments"), json); + } + + @Test + void classicExistingClusterIdIsNotTreatedAsServerlessOrNewCluster() throws Exception { + var target = + DatabricksJobSpecFactory.resolveClusterTarget( + "0718-abcdef-classic1", null, null, null, null, "default", "4"); + assertEquals(DatabricksJobSpecFactory.ComputeMode.EXISTING_CLUSTER, target.mode()); + assertEquals("0718-abcdef-classic1", target.existingClusterId()); + String json = + DatabricksJobSpecFactory.buildCreateJobJson( + "hop-job", + target, + "/Volumes/c/s/v/j.jar", + "/Volumes/c/s/v/p.hpl", + "/Volumes/c/s/v/m.json", + "rc"); + assertTrue(json.contains("existing_cluster_id"), json); + assertTrue(json.contains("\"libraries\""), json); + assertFalse(json.contains("environment_key"), json); + assertFalse(json.contains("new_cluster"), json); + } + + @Test + void resetJobJsonIncludesJobId() throws Exception { + String json = + DatabricksJobSpecFactory.buildResetJobJson( + 42L, "n", "c", "/a.jar", "/b.hpl", "/c.json", "rc"); + assertTrue(json.contains("\"job_id\":42") || json.contains("\"job_id\": 42")); + assertTrue(json.contains("new_settings")); + } + + @Test + void toDbfsUriNormalizes() throws Exception { + assertEquals("dbfs:/x", DatabricksJobSpecFactory.toDbfsUri("/x")); + assertEquals("dbfs:/x", DatabricksJobSpecFactory.toDbfsUri("dbfs:/x")); + } + + @Test + void toDbfsUriKeepsVolumePathsWithoutDbfsScheme() throws Exception { + assertEquals( + "/Volumes/apache-hop/default/jars/hop-native.jar", + DatabricksJobSpecFactory.toDbfsUri("/Volumes/apache-hop/default/jars/hop-native.jar")); + assertEquals( + "/Volumes/apache-hop/default/jars/hop-native.jar", + DatabricksJobSpecFactory.toDbfsUri("dbfs:/Volumes/apache-hop/default/jars/hop-native.jar")); + } + + @Test + void createJobJsonVolumePathsDoNotGetDbfsScheme() throws Exception { + String json = + DatabricksJobSpecFactory.buildCreateJobJson( + "hop-job", + "cluster-1", + "/Volumes/c/s/v/j.jar", + "/Volumes/c/s/v/p.hpl", + "/Volumes/c/s/v/m.json", + "spark-local"); + // json-simple may escape slashes as \/ + assertTrue(json.contains("Volumes") && json.contains("j.jar"), json); + assertFalse(json.contains("dbfs:"), json); + } + + @Test + void missingClusterFails() { + assertThrows( + HopException.class, + () -> DatabricksJobSpecFactory.buildCreateJobJson("n", "", "/j", "/p", "/m", "rc")); + } + + @Test + void namedParametersForProjectPackageAndEnv() throws Exception { + MainSparkLaunchSpec launch = + new MainSparkLaunchSpec( + "pipelines/hello.hpl", + null, + "databricks-native", + "/Volumes/c/s/v/hop-spark-package.zip", + "/Volumes/c/s/v/env-config.json"); + var params = DatabricksJobSpecFactory.buildMainSparkParameters(launch); + assertEquals(4, params.size()); + assertEquals("--HopProjectPackage=/Volumes/c/s/v/hop-spark-package.zip", params.get(0)); + assertEquals("--HopPipelinePath=pipelines/hello.hpl", params.get(1)); + assertEquals("--HopRunConfigurationName=databricks-native", params.get(2)); + assertEquals("--HopConfigFile=/Volumes/c/s/v/env-config.json", params.get(3)); + } + + @Test + void envOnlyUsesNamedParametersWithMetadata() throws Exception { + MainSparkLaunchSpec launch = + new MainSparkLaunchSpec( + "/Volumes/c/s/v/pipeline.hpl", + "/Volumes/c/s/v/metadata.json", + "rc", + null, + "/Volumes/c/s/v/env-config.json"); + var params = DatabricksJobSpecFactory.buildMainSparkParameters(launch); + String joined = params.toString(); + assertTrue(joined.contains("--HopPipelinePath="), joined); + assertTrue(joined.contains("--HopMetadataPath="), joined); + assertTrue(joined.contains("--HopConfigFile="), joined); + assertFalse(joined.contains("--HopProjectPackage="), joined); + } + + @Test + void classicPositionalWhenNoPackageOrEnv() throws Exception { + MainSparkLaunchSpec launch = + MainSparkLaunchSpec.positional("/Volumes/c/s/v/p.hpl", "/Volumes/c/s/v/m.json", "rc"); + var params = DatabricksJobSpecFactory.buildMainSparkParameters(launch); + assertEquals(3, params.size()); + assertEquals("/Volumes/c/s/v/p.hpl", params.get(0)); + assertEquals("/Volumes/c/s/v/m.json", params.get(1)); + assertEquals("rc", params.get(2)); + } + + @Test + void newClusterTokenEmitsNestedObjectNotExistingClusterId() throws Exception { + var cluster = + DatabricksJobSpecFactory.buildNewClusterObject("18.2.x-scala2.13", "i3.xlarge", "1", null); + String json = + DatabricksJobSpecFactory.buildCreateJobJson( + "hop-job", + DatabricksJobSpecFactory.NEW_CLUSTER_TOKEN, + cluster, + "/Volumes/c/s/v/j.jar", + "/Volumes/c/s/v/p.hpl", + "/Volumes/c/s/v/m.json", + "spark-local"); + assertTrue(json.contains("new_cluster"), json); + assertTrue(json.contains("18.2.x-scala2.13"), json); + assertTrue(json.contains("i3.xlarge"), json); + assertFalse(json.contains("existing_cluster_id"), json); + // Must not send the sentinel as a fake cluster id + assertFalse(json.contains("\"existing_cluster_id\":\"new_cluster\""), json); + } + + @Test + void resolveClusterTargetBuildsJobCluster() throws Exception { + var target = + DatabricksJobSpecFactory.resolveClusterTarget( + "new_cluster", "18.2.x-scala2.13", "i3.xlarge", "2", null, "default", "4"); + assertEquals(DatabricksJobSpecFactory.ComputeMode.NEW_CLUSTER, target.mode()); + assertEquals("18.2.x-scala2.13", target.newCluster().get("spark_version")); + assertEquals("i3.xlarge", target.newCluster().get("node_type_id")); + assertEquals(2L, ((Number) target.newCluster().get("num_workers")).longValue()); + } + + @Test + void newClusterJsonOverrideWins() throws Exception { + var cluster = + DatabricksJobSpecFactory.buildNewClusterObject( + "ignored", + "ignored", + "9", + "{\"spark_version\":\"18.2.x-photon-scala2.13\",\"node_type_id\":\"i3.xlarge\",\"num_workers\":0}"); + assertEquals("18.2.x-photon-scala2.13", cluster.get("spark_version")); + assertEquals("i3.xlarge", cluster.get("node_type_id")); + } + + @Test + void serverlessEmitsEnvironmentsAndEnvironmentKeyWithoutClusterFields() throws Exception { + var target = + DatabricksJobSpecFactory.resolveClusterTarget( + "serverless", null, null, null, null, "default", "4"); + assertEquals(DatabricksJobSpecFactory.ComputeMode.SERVERLESS, target.mode()); + String json = + DatabricksJobSpecFactory.buildCreateJobJson( + "My Hop Job", + target, + "/Volumes/apache-hop/default/jars/hop-native.jar", + "/Volumes/apache-hop/default/jars/pipeline.hpl", + "/Volumes/apache-hop/default/jars/metadata.json", + "databricks-native"); + assertTrue(json.contains("environments"), json); + assertTrue(json.contains("environment_key"), json); + assertTrue(json.contains("\"client\":\"4\"") || json.contains("\"client\": \"4\""), json); + assertTrue(json.contains("java_dependencies"), json); + assertTrue(json.contains("hop-native.jar"), json); + // Serverless: jar in environment java_dependencies, not task libraries + assertFalse(json.contains("\"libraries\""), json); + assertFalse(json.contains("existing_cluster_id"), json); + assertFalse(json.contains("new_cluster"), json); + assertTrue(json.contains("MainSpark"), json); + } +} diff --git a/plugins/tech/databricks/src/test/java/org/apache/hop/databricks/deploy/HopSparkDeployHelperTest.java b/plugins/tech/databricks/src/test/java/org/apache/hop/databricks/deploy/HopSparkDeployHelperTest.java new file mode 100644 index 00000000000..018963b27b0 --- /dev/null +++ b/plugins/tech/databricks/src/test/java/org/apache/hop/databricks/deploy/HopSparkDeployHelperTest.java @@ -0,0 +1,307 @@ +/* + * 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.hop.databricks.deploy; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import org.apache.hop.core.logging.ILogChannel; +import org.apache.hop.databricks.client.DatabricksJobsClient; +import org.apache.hop.databricks.client.WorkspaceFileMetadata; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class HopSparkDeployHelperTest { + + @TempDir Path tempDir; + + @Test + void normalizeSha256AcceptsChecksumOnlyAndChecksumFilename() { + assertEquals("abc123", HopSparkDeployHelper.normalizeSha256("ABC123\n")); + assertEquals("deadbeef", HopSparkDeployHelper.normalizeSha256("deadbeef hop-native.jar")); + assertEquals("cafe", HopSparkDeployHelper.normalizeSha256(" cafe\tfile ")); + } + + @Test + void sha256HexIsStable() throws Exception { + Path f = tempDir.resolve("a.bin"); + Files.writeString(f, "hello-databricks"); + String h1 = HopSparkDeployHelper.sha256Hex(f); + String h2 = HopSparkDeployHelper.sha256Hex(f); + assertEquals(h1, h2); + assertEquals(64, h1.length()); + } + + @Test + void fatJarRemoteNameUsesShaPrefix() throws Exception { + assertEquals( + "hop-native-2f80e51734a5.jar", + HopSparkDeployHelper.fatJarRemoteName( + "2f80e51734a5a939850a50ab92d326c32fc04d1e91ec6947e5a997e512130084")); + } + + @Test + void uploadFatJarSkippedWhenSizeAndSidecarMatch() throws Exception { + Path jar = tempDir.resolve("fat.jar"); + Files.write(jar, "same-content-bytes".getBytes(StandardCharsets.UTF_8)); + long size = Files.size(jar); + String sha = HopSparkDeployHelper.sha256Hex(jar); + + DatabricksJobsClient client = mock(DatabricksJobsClient.class); + when(client.getFileMetadata("/Volumes/c/s/v/hop-native.jar")) + .thenReturn(WorkspaceFileMetadata.ofFile(size)); + when(client.downloadTextIfExists("/Volumes/c/s/v/hop-native.jar.sha256")) + .thenReturn(Optional.of(sha + "\n")); + + HopSparkDeployHelper.uploadFatJarIfNeeded( + client, mock(ILogChannel.class), jar, "/Volumes/c/s/v/hop-native.jar"); + + verify(client, never()).uploadToDbfs(any(), anyString()); + verify(client, never()).uploadText(anyString(), anyString()); + } + + @Test + void uploadFatJarWhenSizeDiffers() throws Exception { + Path jar = tempDir.resolve("fat.jar"); + Files.write(jar, "local-bytes".getBytes(StandardCharsets.UTF_8)); + String sha = HopSparkDeployHelper.sha256Hex(jar); + + DatabricksJobsClient client = mock(DatabricksJobsClient.class); + when(client.getFileMetadata("/Volumes/c/s/v/hop-native.jar")) + .thenReturn(WorkspaceFileMetadata.ofFile(999L)); + + HopSparkDeployHelper.uploadFatJarIfNeeded( + client, mock(ILogChannel.class), jar, "/Volumes/c/s/v/hop-native.jar"); + + verify(client).uploadToDbfs(eq(jar), eq("/Volumes/c/s/v/hop-native.jar")); + verify(client).uploadText(eq("/Volumes/c/s/v/hop-native.jar.sha256"), eq(sha + "\n")); + } + + @Test + void uploadFatJarWhenMissingSidecar() throws Exception { + Path jar = tempDir.resolve("fat.jar"); + Files.write(jar, "local".getBytes(StandardCharsets.UTF_8)); + long size = Files.size(jar); + String sha = HopSparkDeployHelper.sha256Hex(jar); + + DatabricksJobsClient client = mock(DatabricksJobsClient.class); + when(client.getFileMetadata("/Volumes/c/s/v/hop-native.jar")) + .thenReturn(WorkspaceFileMetadata.ofFile(size)); + when(client.downloadTextIfExists("/Volumes/c/s/v/hop-native.jar.sha256")) + .thenReturn(Optional.empty()); + + HopSparkDeployHelper.uploadFatJarIfNeeded( + client, mock(ILogChannel.class), jar, "/Volumes/c/s/v/hop-native.jar"); + + verify(client).uploadToDbfs(eq(jar), eq("/Volumes/c/s/v/hop-native.jar")); + verify(client).uploadText(eq("/Volumes/c/s/v/hop-native.jar.sha256"), eq(sha + "\n")); + } + + @Test + void remoteJarMatchesFalseWhenHashDiffers() throws Exception { + DatabricksJobsClient client = mock(DatabricksJobsClient.class); + when(client.getFileMetadata("r.jar")).thenReturn(WorkspaceFileMetadata.ofFile(10L)); + when(client.downloadTextIfExists("r.jar.sha256")).thenReturn(Optional.of("aa".repeat(32))); + assertFalse( + HopSparkDeployHelper.remoteJarMatches( + client, "r.jar", "r.jar.sha256", 10L, "bb".repeat(32))); + } + + @Test + void remoteJarMatchesTrueWhenHashMatches() throws Exception { + String sha = "ab".repeat(32); + DatabricksJobsClient client = mock(DatabricksJobsClient.class); + when(client.getFileMetadata("r.jar")).thenReturn(WorkspaceFileMetadata.ofFile(10L)); + when(client.downloadTextIfExists("r.jar.sha256")).thenReturn(Optional.of(sha + " name")); + assertTrue(HopSparkDeployHelper.remoteJarMatches(client, "r.jar", "r.jar.sha256", 10L, sha)); + } + + @Test + void remoteArtifactStemFromPipelinePath() { + assertEquals( + "hello-mapping-databricks", + HopSparkDeployHelper.remoteArtifactStem("/home/matt/proj/hello-mapping-databricks.hpl")); + assertEquals("entry", HopSparkDeployHelper.remoteArtifactStem("pipelines/entry.hpl")); + assertEquals("foo-bar", HopSparkDeployHelper.remoteArtifactStem("foo bar.hpl")); + assertEquals("pipeline", HopSparkDeployHelper.remoteArtifactStem("")); + assertEquals( + "hop-spark-package-hello-mapping-databricks.zip", + HopSparkDeployHelper.projectPackageRemoteName("hello-mapping-databricks")); + } + + @Test + void relativePipelinePathUnderProjectHome() throws Exception { + Path home = tempDir.resolve("proj"); + Path pipes = home.resolve("pipelines"); + Files.createDirectories(pipes); + Path hpl = pipes.resolve("hello.hpl"); + Files.writeString(hpl, ""); + String rel = HopSparkDeployHelper.relativePipelinePath(hpl.toString(), home.toString(), null); + assertEquals("pipelines/hello.hpl", rel); + } + + @Test + void relativePipelinePathOutsideProjectReturnsNull() throws Exception { + Path home = tempDir.resolve("proj2"); + Files.createDirectories(home); + Path other = tempDir.resolve("other.hpl"); + Files.writeString(other, ""); + assertEquals( + null, HopSparkDeployHelper.relativePipelinePath(other.toString(), home.toString(), null)); + } + + @Test + void deployWithExistingPackageAndEnvUsesNamedLaunch() throws Exception { + Path jar = tempDir.resolve("fat.jar"); + Files.write(jar, "jar-bytes".getBytes(StandardCharsets.UTF_8)); + Path home = tempDir.resolve("home"); + Path pipes = home.resolve("pipelines"); + Files.createDirectories(pipes); + Path hpl = pipes.resolve("entry.hpl"); + Files.writeString(hpl, ""); + Path pkg = tempDir.resolve("pkg.zip"); + Files.write(pkg, "zip".getBytes(StandardCharsets.UTF_8)); + Path env = tempDir.resolve("env.json"); + Files.writeString(env, "{\"variables\":[]}"); + + DatabricksJobsClient client = mock(DatabricksJobsClient.class); + when(client.getFileMetadata(anyString())).thenReturn(WorkspaceFileMetadata.missing()); + + var artifacts = + HopSparkDeployHelper.deploy( + client, + null, + null, + mock(ILogChannel.class), + jar.toString(), + hpl.toString(), + "native-rc", + "/Volumes/c/s/v", + new HopSparkDeployHelper.DeployOptions( + true, home.toString(), pkg.toString(), env.toString())); + + String expectedJar = + "/Volumes/c/s/v/" + + HopSparkDeployHelper.fatJarRemoteName(HopSparkDeployHelper.sha256Hex(jar)); + assertEquals(expectedJar, artifacts.jarDbfs()); + assertEquals("/Volumes/c/s/v/hop-spark-package-entry.zip", artifacts.projectPackageDbfs()); + assertEquals("/Volumes/c/s/v/env-config-entry.json", artifacts.envConfigDbfs()); + assertEquals(null, artifacts.metadataDbfs()); + assertEquals("pipelines/entry.hpl", artifacts.launch().pipelinePath()); + assertTrue(artifacts.launch().hasProjectPackage()); + assertTrue(artifacts.launch().hasConfigFile()); + assertTrue(artifacts.launch().useNamedParameters()); + + verify(client).uploadToDbfs(eq(pkg), eq("/Volumes/c/s/v/hop-spark-package-entry.zip")); + verify(client).uploadToDbfs(eq(env), eq("/Volumes/c/s/v/env-config-entry.json")); + // relative pipeline under package — no separate pipeline.hpl upload of entry + verify(client, never()).uploadToDbfs(eq(hpl), anyString()); + } + + @Test + void deployWithEnvOnlyStillUploadsPipelineAndMetadata() throws Exception { + Path jar = tempDir.resolve("fat2.jar"); + Files.write(jar, "jar".getBytes(StandardCharsets.UTF_8)); + Path hpl = tempDir.resolve("p.hpl"); + Files.writeString(hpl, ""); + Path env = tempDir.resolve("e.json"); + Files.writeString(env, "{}"); + + // Minimal metadata provider + org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider meta = + new org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider(); + + DatabricksJobsClient client = mock(DatabricksJobsClient.class); + when(client.getFileMetadata(anyString())).thenReturn(WorkspaceFileMetadata.missing()); + + var artifacts = + HopSparkDeployHelper.deploy( + client, + meta, + null, + mock(ILogChannel.class), + jar.toString(), + hpl.toString(), + "rc", + "/Volumes/c/s/v2", + new HopSparkDeployHelper.DeployOptions(false, null, null, env.toString())); + + assertEquals("/Volumes/c/s/v2/pipeline-p.hpl", artifacts.pipelineDbfs()); + assertEquals("/Volumes/c/s/v2/metadata-p.json", artifacts.metadataDbfs()); + assertEquals(null, artifacts.projectPackageDbfs()); + assertEquals("/Volumes/c/s/v2/env-config-p.json", artifacts.envConfigDbfs()); + assertTrue(artifacts.launch().useNamedParameters()); + verify(client).uploadToDbfs(eq(hpl), eq("/Volumes/c/s/v2/pipeline-p.hpl")); + } + + @Test + void concurrentPipelinesGetDistinctPackagePaths() throws Exception { + Path jar = tempDir.resolve("fat3.jar"); + Files.write(jar, "jar".getBytes(StandardCharsets.UTF_8)); + Path home = tempDir.resolve("home3"); + Files.createDirectories(home); + Path a = home.resolve("hello-a.hpl"); + Path b = home.resolve("hello-b.hpl"); + Files.writeString(a, ""); + Files.writeString(b, ""); + Path pkg = tempDir.resolve("shared.zip"); + Files.write(pkg, "z".getBytes(StandardCharsets.UTF_8)); + + DatabricksJobsClient client = mock(DatabricksJobsClient.class); + when(client.getFileMetadata(anyString())).thenReturn(WorkspaceFileMetadata.missing()); + + var artA = + HopSparkDeployHelper.deploy( + client, + null, + null, + mock(ILogChannel.class), + jar.toString(), + a.toString(), + "rc", + "/Volumes/v", + new HopSparkDeployHelper.DeployOptions(true, home.toString(), pkg.toString(), null)); + var artB = + HopSparkDeployHelper.deploy( + client, + null, + null, + mock(ILogChannel.class), + jar.toString(), + b.toString(), + "rc", + "/Volumes/v", + new HopSparkDeployHelper.DeployOptions(true, home.toString(), pkg.toString(), null)); + + assertEquals("/Volumes/v/hop-spark-package-hello-a.zip", artA.projectPackageDbfs()); + assertEquals("/Volumes/v/hop-spark-package-hello-b.zip", artB.projectPackageDbfs()); + assertTrue(!artA.projectPackageDbfs().equals(artB.projectPackageDbfs())); + } +} diff --git a/plugins/tech/databricks/src/test/java/org/apache/hop/workflow/actions/databricks/ActionDatabricksJobRunTest.java b/plugins/tech/databricks/src/test/java/org/apache/hop/workflow/actions/databricks/ActionDatabricksJobRunTest.java new file mode 100644 index 00000000000..5c777f450b8 --- /dev/null +++ b/plugins/tech/databricks/src/test/java/org/apache/hop/workflow/actions/databricks/ActionDatabricksJobRunTest.java @@ -0,0 +1,199 @@ +/* + * 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.hop.workflow.actions.databricks; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.apache.hop.core.Result; +import org.apache.hop.databricks.client.DatabricksJobsClient; +import org.apache.hop.databricks.client.DatabricksRunLifeCycleState; +import org.apache.hop.databricks.client.DatabricksRunStatus; +import org.apache.hop.databricks.metadata.DatabricksConnection; +import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension; +import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ActionDatabricksJobRunTest { + + @RegisterExtension + static RestoreHopEngineEnvironmentExtension env = new RestoreHopEngineEnvironmentExtension(); + + private MemoryMetadataProvider metadataProvider; + private DatabricksJobsClient client; + + @BeforeEach + void setUp() throws Exception { + metadataProvider = new MemoryMetadataProvider(); + DatabricksConnection conn = new DatabricksConnection(); + conn.setName("dbx"); + conn.setHost("https://example.databricks.net"); + conn.setToken("tok"); + metadataProvider.getSerializer(DatabricksConnection.class).save(conn); + + client = mock(DatabricksJobsClient.class); + } + + @Test + void cloneCopiesFields() { + ActionDatabricksJobRun action = new ActionDatabricksJobRun("run"); + action.setConnectionName("dbx"); + action.setJobId("12"); + action.setRunMode(ActionDatabricksJobRun.MODE_RUN_EXISTING); + ActionDatabricksJobRun clone = (ActionDatabricksJobRun) action.clone(); + assertEquals("dbx", clone.getConnectionName()); + assertEquals("12", clone.getJobId()); + } + + @Test + void failsWithoutConnection() { + ActionDatabricksJobRun action = new ActionDatabricksJobRun("run"); + action.setMetadataProvider(metadataProvider); + Result result = new Result(); + action.execute(result, 0); + assertEquals(1, result.getNrErrors()); + assertFalse(result.getResult()); + } + + @Test + void runExistingWaitSuccess() throws Exception { + when(client.runNow(anyLong(), anyMap())).thenReturn(100L); + when(client.getRun(100L)) + .thenReturn( + new DatabricksRunStatus( + 100L, + 12L, + DatabricksRunLifeCycleState.TERMINATED, + "SUCCESS", + "ok", + "https://example/runs/100")); + + ActionDatabricksJobRun action = new ActionDatabricksJobRun("run"); + action.setMetadataProvider(metadataProvider); + action.setConnectionName("dbx"); + action.setJobId("12"); + action.setRunMode(ActionDatabricksJobRun.MODE_RUN_EXISTING); + action.setWaitMode(ActionDatabricksJobRun.WAIT_WAIT); + action.setPollIntervalSeconds("1"); + action.setTimeoutSeconds("30"); + action.setClientFactory((c, v) -> client); + + Result result = new Result(); + action.execute(result, 0); + + assertTrue(result.getResult()); + assertEquals(0, result.getNrErrors()); + assertEquals("12", action.getVariable("DatabricksJobId")); + assertEquals("100", action.getVariable("DatabricksRunId")); + assertEquals("SUCCESS", action.getVariable("DatabricksStatus")); + verify(client).runNow(12L, java.util.Map.of()); + verify(client).getRun(100L); + } + + @Test + void fireAndForgetDoesNotPoll() throws Exception { + when(client.runNow(anyLong(), anyMap())).thenReturn(5L); + + ActionDatabricksJobRun action = new ActionDatabricksJobRun("run"); + action.setMetadataProvider(metadataProvider); + action.setConnectionName("dbx"); + action.setJobId("9"); + action.setWaitMode(ActionDatabricksJobRun.WAIT_FIRE_AND_FORGET); + action.setClientFactory((c, v) -> client); + + Result result = new Result(); + action.execute(result, 0); + + assertTrue(result.getResult()); + assertEquals("5", action.getVariable("DatabricksRunId")); + verify(client, never()).getRun(anyLong()); + } + + @Test + void submitOnceUsesSubmitJson() throws Exception { + when(client.submitRun(anyString())).thenReturn(77L); + when(client.getRun(77L)) + .thenReturn( + new DatabricksRunStatus( + 77L, null, DatabricksRunLifeCycleState.TERMINATED, "SUCCESS", null, null)); + + ActionDatabricksJobRun action = new ActionDatabricksJobRun("run"); + action.setMetadataProvider(metadataProvider); + action.setConnectionName("dbx"); + action.setRunMode(ActionDatabricksJobRun.MODE_SUBMIT_ONCE); + action.setSubmitRunJson("{\"tasks\":[]}"); + action.setWaitMode(ActionDatabricksJobRun.WAIT_WAIT); + action.setPollIntervalSeconds("1"); + action.setClientFactory((c, v) -> client); + + Result result = new Result(); + action.execute(result, 0); + + assertTrue(result.getResult()); + verify(client).submitRun("{\"tasks\":[]}"); + } + + @Test + void deployAndRunRequiresClusterId() { + ActionDatabricksJobRun action = new ActionDatabricksJobRun("run"); + action.setMetadataProvider(metadataProvider); + action.setConnectionName("dbx"); + action.setRunMode(ActionDatabricksJobRun.MODE_DEPLOY_AND_RUN); + action.setClientFactory((c, v) -> client); + Result r = new Result(); + action.execute(r, 0); + assertFalse(r.getResult()); + assertEquals(1, r.getNrErrors()); + } + + @Test + void failedRunMarksResultFalse() throws Exception { + when(client.runNow(anyLong(), anyMap())).thenReturn(1L); + when(client.getRun(1L)) + .thenReturn( + new DatabricksRunStatus( + 1L, 2L, DatabricksRunLifeCycleState.TERMINATED, "FAILED", "boom", null)); + + ActionDatabricksJobRun action = new ActionDatabricksJobRun("run"); + action.setMetadataProvider(metadataProvider); + action.setConnectionName("dbx"); + action.setJobId("2"); + action.setWaitMode(ActionDatabricksJobRun.WAIT_WAIT); + action.setPollIntervalSeconds("1"); + action.setClientFactory((c, v) -> client); + + Result result = new Result(); + action.execute(result, 0); + + assertFalse(result.getResult()); + assertEquals(1, result.getNrErrors()); + assertEquals("FAILED", action.getVariable("DatabricksStatus")); + assertNotNull(action.getVariable("DatabricksError")); + } +} diff --git a/plugins/tech/databricks/src/test/java/org/apache/hop/workflow/actions/databricks/ActionDatabricksJobWaitTest.java b/plugins/tech/databricks/src/test/java/org/apache/hop/workflow/actions/databricks/ActionDatabricksJobWaitTest.java new file mode 100644 index 00000000000..a769214d119 --- /dev/null +++ b/plugins/tech/databricks/src/test/java/org/apache/hop/workflow/actions/databricks/ActionDatabricksJobWaitTest.java @@ -0,0 +1,111 @@ +/* + * 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.hop.workflow.actions.databricks; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.apache.hop.core.Result; +import org.apache.hop.databricks.client.DatabricksJobsClient; +import org.apache.hop.databricks.client.DatabricksRunLifeCycleState; +import org.apache.hop.databricks.client.DatabricksRunStatus; +import org.apache.hop.databricks.metadata.DatabricksConnection; +import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension; +import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class ActionDatabricksJobWaitTest { + + @RegisterExtension + static RestoreHopEngineEnvironmentExtension env = new RestoreHopEngineEnvironmentExtension(); + + private MemoryMetadataProvider metadataProvider; + private DatabricksJobsClient client; + + @BeforeEach + void setUp() throws Exception { + metadataProvider = new MemoryMetadataProvider(); + DatabricksConnection conn = new DatabricksConnection(); + conn.setName("dbx"); + conn.setHost("https://example.databricks.net"); + conn.setToken("tok"); + metadataProvider.getSerializer(DatabricksConnection.class).save(conn); + client = mock(DatabricksJobsClient.class); + } + + @Test + void waitsForSuccess() throws Exception { + when(client.getRun(anyLong())) + .thenReturn( + new DatabricksRunStatus( + 55L, 3L, DatabricksRunLifeCycleState.TERMINATED, "SUCCESS", null, "http://x")); + + ActionDatabricksJobWait action = new ActionDatabricksJobWait("wait"); + action.setMetadataProvider(metadataProvider); + action.setConnectionName("dbx"); + action.setRunId("55"); + action.setPollIntervalSeconds("1"); + action.setClientFactory((c, v) -> client); + + Result result = new Result(); + action.execute(result, 0); + + assertTrue(result.getResult()); + assertEquals("SUCCESS", action.getVariable("DatabricksStatus")); + assertEquals("55", action.getVariable("DatabricksRunId")); + } + + @Test + void failsWithoutRunId() { + ActionDatabricksJobWait action = new ActionDatabricksJobWait("wait"); + action.setMetadataProvider(metadataProvider); + action.setConnectionName("dbx"); + action.setRunId(""); + action.setClientFactory((c, v) -> client); + + Result result = new Result(); + action.execute(result, 0); + assertFalse(result.getResult()); + assertEquals(1, result.getNrErrors()); + } + + @Test + void failedStatusMarksActionFailed() throws Exception { + when(client.getRun(anyLong())) + .thenReturn( + new DatabricksRunStatus( + 1L, 2L, DatabricksRunLifeCycleState.TERMINATED, "FAILED", "nope", null)); + + ActionDatabricksJobWait action = new ActionDatabricksJobWait("wait"); + action.setMetadataProvider(metadataProvider); + action.setConnectionName("dbx"); + action.setRunId("1"); + action.setClientFactory((c, v) -> client); + + Result result = new Result(); + action.execute(result, 0); + assertFalse(result.getResult()); + assertEquals("FAILED", action.getVariable("DatabricksStatus")); + } +} diff --git a/plugins/tech/google/src/main/java/org/apache/hop/pipeline/transforms/googlesheets/GoogleSheetsCredentials.java b/plugins/tech/google/src/main/java/org/apache/hop/pipeline/transforms/googlesheets/GoogleSheetsCredentials.java index 2ecf8a9075f..64889ce4477 100644 --- a/plugins/tech/google/src/main/java/org/apache/hop/pipeline/transforms/googlesheets/GoogleSheetsCredentials.java +++ b/plugins/tech/google/src/main/java/org/apache/hop/pipeline/transforms/googlesheets/GoogleSheetsCredentials.java @@ -61,7 +61,17 @@ public static HttpCredentialsAdapter getCredentialsJson( throw new FileNotFoundException("Resource not found:" + jsonCredentialPath); } - credential = GoogleCredentials.fromStream(in); + try { + credential = GoogleCredentials.fromStream(in); + } catch (IOException e) { + throw new IOException( + "Unable to load Google credentials from '" + + jsonCredentialPath + + "'. Expected a service-account JSON key file (as downloaded from Google Cloud). " + + "Integration tests require GCP_KEY_FILE pointing at that JSON (Jenkins uses " + + "credentials 'gcp-access-hop'); the default dummy file is not valid JSON.", + e); + } if (httpTransport != null) { HttpTransportFactory proxyTransportFactory = () -> httpTransport; diff --git a/plugins/tech/google/src/main/java/org/apache/hop/vfs/gs/GoogleStorageFileProvider.java b/plugins/tech/google/src/main/java/org/apache/hop/vfs/gs/GoogleStorageFileProvider.java index f6624e861b5..7e6f97d381c 100644 --- a/plugins/tech/google/src/main/java/org/apache/hop/vfs/gs/GoogleStorageFileProvider.java +++ b/plugins/tech/google/src/main/java/org/apache/hop/vfs/gs/GoogleStorageFileProvider.java @@ -6,7 +6,7 @@ * (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 + * 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, @@ -79,20 +79,20 @@ protected FileSystem doCreateFileSystem(FileName rootName, FileSystemOptions fil private void setServiceAccountCredentials( IVariables variables, GoogleStorageMetadataType googleStorageMetadataType) { try { - // Hop configuration options - // - GoogleCredentials credentials = GoogleCredentials.getApplicationDefault(); + GoogleCredentials credentials = null; String scheme = "gs"; if (variables == null && googleStorageMetadataType == null) { - // Default configuration - // If we don't have a setting for a service account key file we try the default - // + // Default configuration: prefer explicit key file; ADC only as optional fallback. + // Do not call ADC first — on non-GCP clusters (e.g. Databricks AWS) it probes metadata + // and can throw NoClassDefFoundError for io.grpc.Context when gRPC is not on the fat jar. GoogleCloudConfig config = GoogleCloudConfigSingleton.getConfig(); if (!StringUtils.isEmpty(config.getServiceAccountKeyFile())) { credentials = ServiceAccountCredentials.fromStream( new FileInputStream(config.getServiceAccountKeyFile())); + } else { + credentials = tryApplicationDefaultCredentials(); } } else { scheme = googleStorageMetadataType.getName(); @@ -111,11 +111,14 @@ private void setServiceAccountCredentials( StandardCharsets.UTF_8)); break; default: + credentials = tryApplicationDefaultCredentials(); break; } } - GoogleStorageFileSystemConfigBuilder.getInstance() - .setGoogleCredentials(newFileSystemOptions, credentials); + if (credentials != null) { + GoogleStorageFileSystemConfigBuilder.getInstance() + .setGoogleCredentials(newFileSystemOptions, credentials); + } GoogleStorageFileSystemConfigBuilder.getInstance().setSchema(newFileSystemOptions, scheme); } catch (Exception e) { // Do not log error for the default GS account @@ -125,6 +128,46 @@ private void setServiceAccountCredentials( + googleStorageMetadataType.getName(), e); } + } catch (LinkageError e) { + // NoClassDefFoundError (e.g. io.grpc.Context) must not prevent HopEnvironment.init + if (googleStorageMetadataType != null) { + LogChannel.GENERAL.logError( + "Unable to set service account credentials for vfs name: " + + googleStorageMetadataType.getName() + + " (missing dependency: " + + e.getMessage() + + ")", + e); + } else { + LogChannel.GENERAL.logDetailed( + "Google Storage VFS: Application Default Credentials unavailable (" + + e.getClass().getSimpleName() + + ": " + + e.getMessage() + + "). gs:// will work after configuring a service account key."); + } + } + } + + /** + * Best-effort ADC. Returns null if unavailable (not on GCE/GCP, or gRPC/auth deps missing from + * classpath — common with native-provided fat jars on Databricks/AWS). + */ + static GoogleCredentials tryApplicationDefaultCredentials() { + try { + return GoogleCredentials.getApplicationDefault(); + } catch (Exception e) { + LogChannel.GENERAL.logDetailed( + "Google Storage VFS: Application Default Credentials not available: " + e.getMessage()); + return null; + } catch (LinkageError e) { + LogChannel.GENERAL.logDetailed( + "Google Storage VFS: Application Default Credentials not available (" + + e.getClass().getSimpleName() + + ": " + + e.getMessage() + + ")"); + return null; } } } diff --git a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByMeta.java b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByMeta.java index 64b2983f96f..1360b542cb1 100644 --- a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByMeta.java +++ b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByMeta.java @@ -46,7 +46,9 @@ categoryDescription = "i18n:org.apache.hop.pipeline.transform:BaseTransform.Category.Statistics", documentationUrl = "/pipeline/transforms/groupby.html", - keywords = "i18n::GroupByMeta.keyword") + keywords = "i18n::GroupByMeta.keyword", + // Must match SparkConst.PLUGIN_ID — do not import engines-spark from transform modules. + excludedEngines = {"SparkPipelineEngine"}) public class GroupByMeta extends BaseTransformMeta { private static final Class PKG = GroupByMeta.class; diff --git a/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsonoutput/JsonOutput.java b/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsonoutput/JsonOutput.java index c35c9f2db64..12039a113f1 100644 --- a/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsonoutput/JsonOutput.java +++ b/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsonoutput/JsonOutput.java @@ -280,22 +280,34 @@ private void createParentFolder(String filename) throws HopTransformException { FileObject parentfolder = null; try { // Get parent folder - parentfolder = HopVfs.getFileObject(filename).getParent(); + parentfolder = HopVfs.getFileObject(filename, variables).getParent(); + if (parentfolder == null) { + throw new HopTransformException( + BaseMessages.getString(PKG, "JsonOutput.Error.ErrorCreatingParentFolder", filename)); + } if (!parentfolder.exists()) { + String parentUri = HopVfs.getFriendlyURI(parentfolder); if (isDebug()) { - logDebug( - BaseMessages.getString( - PKG, "JsonOutput.Error.ParentFolderNotExist", parentfolder.getName())); + logDebug(BaseMessages.getString(PKG, "JsonOutput.Error.ParentFolderNotExist", parentUri)); + } + try { + parentfolder.createFolder(); + } catch (Exception createEx) { + // Another concurrent writer (e.g. Beam parallel outputs) may have created it + parentfolder = HopVfs.getFileObject(filename, variables).getParent(); + if (parentfolder == null || !parentfolder.exists()) { + throw createEx; + } } - parentfolder.createFolder(); if (isDebug()) { logDebug(BaseMessages.getString(PKG, "JsonOutput.Log.ParentFolderCreated")); } } } catch (Exception e) { + String parentDesc = + parentfolder != null ? HopVfs.getFriendlyURI(parentfolder) : String.valueOf(filename); throw new HopTransformException( - BaseMessages.getString( - PKG, "JsonOutput.Error.ErrorCreatingParentFolder", parentfolder.getName())); + BaseMessages.getString(PKG, "JsonOutput.Error.ErrorCreatingParentFolder", parentDesc), e); } finally { if (parentfolder != null) { try { @@ -352,7 +364,7 @@ public boolean openNewFile() { retval = true; } catch (Exception e) { - logError(BaseMessages.getString(PKG, "JsonOutput.Error.OpeningFile", e.toString())); + logError(BaseMessages.getString(PKG, "JsonOutput.Error.OpeningFile", e.toString()), e); } return retval; diff --git a/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsonoutputenhanced/JsonEOutput.java b/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsonoutputenhanced/JsonEOutput.java index 2bbff847eba..31d29c9ae06 100644 --- a/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsonoutputenhanced/JsonEOutput.java +++ b/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsonoutputenhanced/JsonEOutput.java @@ -557,27 +557,43 @@ public void dispose() { } private void createParentFolder(String filename) throws HopTransformException { - if (!meta.getFileSettings().isCreateParentFolder()) return; + if (!meta.getFileSettings().isCreateParentFolder()) { + return; + } // Check for parent folder FileObject parentfolder = null; try { // Get parent folder - parentfolder = HopVfs.getFileObject(filename).getParent(); + parentfolder = HopVfs.getFileObject(filename, variables).getParent(); + if (parentfolder == null) { + throw new HopTransformException( + BaseMessages.getString(PKG, "JsonEOutput.Error.ErrorCreatingParentFolder", filename)); + } if (!parentfolder.exists()) { + String parentUri = HopVfs.getFriendlyURI(parentfolder); if (isDebug()) { logDebug( - BaseMessages.getString( - PKG, "JsonOutput.Error.ParentFolderNotExist", parentfolder.getName())); + BaseMessages.getString(PKG, "JsonEOutput.Error.ParentFolderNotExist", parentUri)); + } + try { + parentfolder.createFolder(); + } catch (Exception createEx) { + // Another concurrent writer (e.g. Beam parallel outputs) may have created it + parentfolder = HopVfs.getFileObject(filename, variables).getParent(); + if (parentfolder == null || !parentfolder.exists()) { + throw createEx; + } } - parentfolder.createFolder(); if (isDebug()) { - logDebug(BaseMessages.getString(PKG, "JsonOutput.Log.ParentFolderCreated")); + logDebug(BaseMessages.getString(PKG, "JsonEOutput.Log.ParentFolderCreated")); } } } catch (Exception e) { + String parentDesc = + parentfolder != null ? HopVfs.getFriendlyURI(parentfolder) : String.valueOf(filename); throw new HopTransformException( - BaseMessages.getString( - PKG, "JsonOutput.Error.ErrorCreatingParentFolder", parentfolder.getName())); + BaseMessages.getString(PKG, "JsonEOutput.Error.ErrorCreatingParentFolder", parentDesc), + e); } finally { if (parentfolder != null) { try { diff --git a/plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsonoutput/messages/messages_en_US.properties b/plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsonoutput/messages/messages_en_US.properties index f933fc291b0..a991ef01c18 100644 --- a/plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsonoutput/messages/messages_en_US.properties +++ b/plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsonoutput/messages/messages_en_US.properties @@ -19,11 +19,11 @@ JsonOutput.category=Output JsonOutput.description=Create JSON block and output it in a field or a file. JsonOutput.Error.ClosingFile=Exception trying to close file: {0} -JsonOutput.Error.ErrorCreatingParentFolder=Couldn't create parent folder [{0}]! +JsonOutput.Error.ErrorCreatingParentFolder=Could not create parent folder [{0}]! JsonOutput.Error.MissingOutputFieldName=Output fieldname that will contain value is empty\! JsonOutput.Error.MissingTargetFilename=Target filename is missing\! JsonOutput.Error.OpeningFile=Error opening new file : {0} -JsonOutput.Error.OpenNewFile=Couldn't open file [{0}]! +JsonOutput.Error.OpenNewFile=Could not open file [{0}]! JsonOutput.Error.ParentFolderNotExist=Folder parent [{0}] does not exist! JsonOutput.Error.Writing=Error writing to file! JsonOutput.Exception.FieldNotFound=The specified field ''{0}'' could not be found in the input. diff --git a/plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsonoutput/messages/messages_ja_JP.properties b/plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsonoutput/messages/messages_ja_JP.properties index ef199509788..747bb45a101 100644 --- a/plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsonoutput/messages/messages_ja_JP.properties +++ b/plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsonoutput/messages/messages_ja_JP.properties @@ -20,11 +20,11 @@ JsonOutput.category=\u51FA\u529B\u3000 JsonOutput.description=JSON output\nJSON\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u306E\u751F\u6210\u3092\u884C\u3044\u51FA\u529B\u3057\u307E\u3059\u3002 JsonOutput.Error.ClosingFile=Exception trying to close file: {0} -JsonOutput.Error.ErrorCreatingParentFolder=Couldn't created parent folder [{0}]! +JsonOutput.Error.ErrorCreatingParentFolder=Could not create parent folder [{0}]! JsonOutput.Error.MissingOutputFieldName=Output fieldname that will contain value is empty\! JsonOutput.Error.MissingTargetFilename=Target filename is missing\! JsonOutput.Error.OpeningFile=Error opening new file : {0} -JsonOutput.Error.OpenNewFile=Couldn't open file [{0}]! +JsonOutput.Error.OpenNewFile=Could not open file [{0}]! JsonOutput.Error.ParentFolderNotExist=Folder parent [{0}] does not exist! JsonOutput.Error.Writing=Error writing to file! JsonOutput.Exception.FieldNotFound=The specified field ''{0}'' could not be found in the input. diff --git a/plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsonoutput/messages/messages_zh_CN.properties b/plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsonoutput/messages/messages_zh_CN.properties index ece2fd015a3..2459f4a389a 100644 --- a/plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsonoutput/messages/messages_zh_CN.properties +++ b/plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsonoutput/messages/messages_zh_CN.properties @@ -24,7 +24,7 @@ JsonOutput.Error.ErrorCreatingParentFolder=\u65E0\u6CD5\u521B\u5EFA\u4E0A\u7EA7\ JsonOutput.Error.MissingOutputFieldName=Output fieldname that will contain value is empty\! JsonOutput.Error.MissingTargetFilename=Target filename is missing\! JsonOutput.Error.OpeningFile=Error opening new file \: {0} -JsonOutput.Error.OpenNewFile=Couldn't open file [{0}]\! +JsonOutput.Error.OpenNewFile=Could not open file [{0}]\! JsonOutput.Error.ParentFolderNotExist=\u4E0A\u7EA7\u76EE\u5F55 [{0}] \u4E0D\u5B58\u5728! JsonOutput.Error.Writing=Error writing to file\! JsonOutput.Exception.FieldNotFound=\u4ECE\u524D\u7F6E\u901A\u9053\u4E2D\u627E\u4E0D\u5230\u6307\u5B9A\u5B57\u6BB5 "{0}". diff --git a/plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsonoutputenhanced/messages/messages_en_US.properties b/plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsonoutputenhanced/messages/messages_en_US.properties index 8b152b8bf85..6c4b0972d3b 100644 --- a/plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsonoutputenhanced/messages/messages_en_US.properties +++ b/plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsonoutputenhanced/messages/messages_en_US.properties @@ -21,11 +21,11 @@ EnhancedJsonOutput.description=Create JSON block and output it in a field or a f EnhancedJsonOutput.name=Enhanced JSON Output JsonEOutput.Error.Casting=Error casting String to JSON Fragment JsonEOutput.Error.ClosingFile=Exception trying to close file: {0} -JsonEOutput.Error.ErrorCreatingParentFolder=Couldn't create parent folder [{0}]! +JsonEOutput.Error.ErrorCreatingParentFolder=Could not create parent folder [{0}]! JsonEOutput.Error.MissingOutputFieldName=Output fieldname that will contain value is empty\! JsonEOutput.Error.MissingTargetFilename=Target filename is missing\! JsonEOutput.Error.OpeningFile=Error opening new file : {0} -JsonEOutput.Error.OpenNewFile=Couldn't open file [{0}]! +JsonEOutput.Error.OpenNewFile=Could not open file [{0}]! JsonEOutput.Error.ParentFolderNotExist=Folder parent [{0}] does not exist! JsonEOutput.Error.Writing=Error writing to file! JsonEOutput.Exception.FieldNotFound=The specified field ''{0}'' could not be found in the input. diff --git a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/SimpleMapping.java b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/SimpleMapping.java index 74dea6871d2..53fcd4f548e 100644 --- a/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/SimpleMapping.java +++ b/plugins/transforms/mapping/src/main/java/org/apache/hop/pipeline/transforms/mapping/SimpleMapping.java @@ -309,8 +309,17 @@ && getPipeline().getPipelineMeta().getPipelineType() return false; } } catch (Exception e) { - logError("Unable to load the mapping pipeline because of an error : " + e.toString()); + // Include resolved path + PROJECT_HOME so Spark executors surface the real cause + // (missing package file, wrong PROJECT_HOME) instead of only a later dispose() NPE. + logError( + "Unable to load the mapping pipeline '" + + resolve(meta.getFilename()) + + "' (PROJECT_HOME=" + + getVariable("PROJECT_HOME") + + "): " + + e); logError(Const.getStackTracker(e)); + setErrors(1); } } return false; @@ -318,28 +327,32 @@ && getPipeline().getPipelineMeta().getPipelineType() @Override public void dispose() { - if (data.executor != null) { - try { - data.executor.dispose(); - } catch (Exception e) { - logError("Error calling dispose() on single threaded Simple Mapping executor", e); - setErrors(1); - } - } else { - // Close the running pipeline - if (data.wasStarted && !data.mappingPipeline.isFinished()) { - // Wait until the child pipeline has finished. - data.mappingPipeline.waitUntilFinished(); + // mappingPipeline is null when init() failed before prepareMappingExecution completed + // (e.g. child .hpl not found). Pipeline.prepareExecution still calls dispose() on every + // transform — must not NPE and mask the original init error. + try { + if (data.executor != null) { + try { + data.executor.dispose(); + } catch (Exception e) { + logError("Error calling dispose() on single threaded Simple Mapping executor", e); + setErrors(1); + } + } else if (data.mappingPipeline != null) { + // Close the running pipeline + if (data.wasStarted && !data.mappingPipeline.isFinished()) { + // Wait until the child pipeline has finished. + data.mappingPipeline.waitUntilFinished(); + } + // See if there was an error in the sub-pipeline, in that case, flag error etc. + if (data.mappingPipeline.getErrors() > 0) { + logError(BaseMessages.getString(PKG, "SimpleMapping.Log.ErrorOccurredInSubPipeline")); + setErrors(1); + } } + } finally { + super.dispose(); } - - // See if there was an error in the sub-pipeline, in that case, flag error etc. - if (getData().mappingPipeline.getErrors() > 0) { - logError(BaseMessages.getString(PKG, "SimpleMapping.Log.ErrorOccurredInSubPipeline")); - setErrors(1); - } - - super.dispose(); } @Override diff --git a/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/mapping/SimpleMappingTest.java b/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/mapping/SimpleMappingTest.java index 95ba50c19d3..4bd2e0da4ce 100644 --- a/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/mapping/SimpleMappingTest.java +++ b/plugins/transforms/mapping/src/test/java/org/apache/hop/pipeline/transforms/mapping/SimpleMappingTest.java @@ -153,7 +153,7 @@ void testTransformShouldProcessError_WhenMappingPipelineHasError() throws HopExc 0, transformMockHelper.pipelineMeta, transformMockHelper.pipeline); - smp.init(); + // mappingPipeline already set by @BeforeEach; dispose path under test (not full init) smp.dispose(); verify(transformMockHelper.pipeline, times(1)).isFinished(); @@ -196,7 +196,7 @@ void testTransformShouldStopProcessingInput_IfUnderlyingTransitionIsStopped() th 0, transformMockHelper.pipelineMeta, transformMockHelper.pipeline); - smp.init(); + // Use pre-mocked mappingPipeline from iTransformData (full init needs a real mapping .hpl) smp.addRowSetToInputRowSets(transformMockHelper.getMockInputRowSet(new Object[] {})); smp.addRowSetToInputRowSets(transformMockHelper.getMockInputRowSet(new Object[] {})); @@ -215,6 +215,7 @@ void testDispose() throws HopException { // The transform was started simpleMpData.wasStarted = true; + // mappingPipeline is already set up by @BeforeEach (do not call init — needs a real .hpl) smp = new SimpleMapping( transformMockHelper.transformMeta, @@ -223,10 +224,25 @@ void testDispose() throws HopException { 0, transformMockHelper.pipelineMeta, transformMockHelper.pipeline); - smp.init(); smp.dispose(); verify(transformMockHelper.pipeline, times(1)).isFinished(); verify(transformMockHelper.pipeline, times(1)).waitUntilFinished(); } + + @Test + void disposeWithNullMappingPipelineDoesNotNpe() { + // Failed init leaves mappingPipeline null; Pipeline.prepareExecution still calls dispose(). + simpleMpData.mappingPipeline = null; + simpleMpData.wasStarted = false; + smp = + new SimpleMapping( + transformMockHelper.transformMeta, + transformMockHelper.iTransformMeta, + simpleMpData, + 0, + transformMockHelper.pipelineMeta, + transformMockHelper.pipeline); + smp.dispose(); + } } diff --git a/plugins/transforms/pipelineexecutor/src/main/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutor.java b/plugins/transforms/pipelineexecutor/src/main/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutor.java index 6689a647ee2..07cef4bc097 100644 --- a/plugins/transforms/pipelineexecutor/src/main/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutor.java +++ b/plugins/transforms/pipelineexecutor/src/main/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutor.java @@ -293,12 +293,11 @@ void discardLogLines(PipelineExecutorData pipelineExecutorData) { IPipelineEngine createInternalPipeline() throws HopException { String runConfigurationName = resolve(meta.getRunConfigurationName()); + // Variable source is this transform so nested engines resolve EXECUTIONS_INFORMATION_FOLDER / + // HOP_DATA the same way when running under Native Spark mapPartitions. IPipelineEngine executorPipeline = PipelineEngineFactory.createPipelineEngine( - getPipeline(), - runConfigurationName, - metadataProvider, - getData().getExecutorPipelineMeta()); + this, runConfigurationName, metadataProvider, getData().getExecutorPipelineMeta()); executorPipeline.setParentPipeline(getPipeline()); executorPipeline.setParent(this); executorPipeline.setLogLevel(getLogLevel()); @@ -404,6 +403,23 @@ void passParametersToPipeline(List incomingFieldValues) { } IPipelineEngine pipeline = getExecutorPipeline(); + + // When a mapped field (or static input) is empty, clear sticky values inherited by the child + // from a previous PipelineExecutor iteration. NamedParameters.activateParameters prefers an + // existing variable over an empty param value when the child parameter has a non-empty default + // (HOSTNAME safety). Without clearing, an empty field mapping would keep the previous row's + // value instead of applying the child default — see IT main-0003-pipeline-pipeline-executor. + for (int i = 0; i < parameters.size(); i++) { + String variableName = parameters.get(i).getVariable(); + if (Utils.isEmpty(variableName)) { + continue; + } + if (Utils.isEmpty(Const.trim(inputFieldValues[i]))) { + pipeline.setVariable(variableName, null); + this.setVariable(variableName, null); + } + } + TransformWithMappingMeta.activateParams( pipeline, pipeline, diff --git a/plugins/transforms/pipelineexecutor/src/main/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutorMeta.java b/plugins/transforms/pipelineexecutor/src/main/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutorMeta.java index 0cbafd24626..c23efc891a0 100644 --- a/plugins/transforms/pipelineexecutor/src/main/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutorMeta.java +++ b/plugins/transforms/pipelineexecutor/src/main/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutorMeta.java @@ -529,6 +529,22 @@ public void searchInfoAndTargetTransforms(List transforms) { TransformMeta.findTransform(transforms, resultFilesTargetTransform); executorsOutputTransformMeta = TransformMeta.findTransform(transforms, executorsOutputTransform); + + // Rebind TARGET streams: getTransformIOMeta() may have been created before names were + // resolved, leaving Stream.transformMeta null (Beam/Spark multi-target discovery). + List targetStreams = getTransformIOMeta().getTargetStreams(); + if (targetStreams.size() > 0) { + targetStreams.get(0).setTransformMeta(executionResultTargetTransformMeta); + } + if (targetStreams.size() > 1) { + targetStreams.get(1).setTransformMeta(outputRowsSourceTransformMeta); + } + if (targetStreams.size() > 2) { + targetStreams.get(2).setTransformMeta(resultFilesTargetTransformMeta); + } + if (targetStreams.size() > 3) { + targetStreams.get(3).setTransformMeta(executorsOutputTransformMeta); + } } @Override diff --git a/plugins/transforms/pipelineexecutor/src/test/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutorMetaCoverageTest.java b/plugins/transforms/pipelineexecutor/src/test/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutorMetaCoverageTest.java index a89c6f34215..894046143b8 100644 --- a/plugins/transforms/pipelineexecutor/src/test/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutorMetaCoverageTest.java +++ b/plugins/transforms/pipelineexecutor/src/test/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutorMetaCoverageTest.java @@ -321,6 +321,22 @@ void getFieldsLeavesRowUnchangedForMainOutput() throws HopTransformException { assertNotNull(row.searchValueMeta("only")); } + @Test + void searchInfoAndTargetTransformsRebindsTargetStreams() { + PipelineExecutorMeta meta = new PipelineExecutorMeta(); + meta.setDefault(); + meta.setExecutionResultTargetTransform("results"); + // Build IO meta before name resolution (streams start with null subjects) + assertNotNull(meta.getTransformIOMeta()); + assertNull(meta.getTransformIOMeta().getTargetStreams().get(0).getTransformMeta()); + + TransformMeta results = new TransformMeta("results", mock(ITransformMeta.class)); + meta.searchInfoAndTargetTransforms(List.of(results)); + + assertEquals(results, meta.getExecutionResultTargetTransformMeta()); + assertEquals(results, meta.getTransformIOMeta().getTargetStreams().get(0).getTransformMeta()); + } + @Test void prepareExecutionResultsFileFieldsAddsFileNameColumn() throws HopTransformException { PipelineExecutorMeta meta = new PipelineExecutorMeta(); diff --git a/plugins/transforms/pipelineexecutor/src/test/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutorTest.java b/plugins/transforms/pipelineexecutor/src/test/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutorTest.java index 88cc9755aaa..dd7a2782bde 100644 --- a/plugins/transforms/pipelineexecutor/src/test/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutorTest.java +++ b/plugins/transforms/pipelineexecutor/src/test/java/org/apache/hop/pipeline/transforms/pipelineexecutor/PipelineExecutorTest.java @@ -208,6 +208,81 @@ void passParametersToPipelineTreatsNullIncomingListAsEmpty() throws HopException verify(child, atLeastOnce()).setParameterValue("P", "fallback"); } + /** + * Empty field mapping must apply the child pipeline parameter default, not a sticky value left + * from a previous PipelineExecutor row (integration test main-0003-pipeline-pipeline-executor). + */ + @Test + void passParametersToPipelineEmptyFieldUsesChildParameterDefaultNotStickyValue() + throws HopException { + PipelineExecutorMeta meta = new PipelineExecutorMeta(); + meta.setDefault(); + meta.setInheritingAllVariables(true); + PipelineExecutorParameters param = new PipelineExecutorParameters(); + param.setVariable("TEST3_PARAMETER2"); + param.setField("value2"); + param.setInput(""); + meta.setParameters(new ArrayList<>(Collections.singletonList(param))); + + RowMeta inputRowMeta = new RowMeta(); + inputRowMeta.addValueMeta(new ValueMetaString("value2")); + + PipelineMeta childMeta = new PipelineMeta(); + childMeta.setName("child"); + childMeta.addParameterDefinition("TEST3_PARAMETER2", "default2", "with default"); + + LocalPipelineEngine child = new LocalPipelineEngine(childMeta); + child.copyParametersFromDefinitions(childMeta); + // Simulate sticky value inherited from a previous executor iteration + child.setVariable("TEST3_PARAMETER2", "B2"); + + PipelineExecutorData data = new PipelineExecutorData(); + data.setInputRowMeta(inputRowMeta); + data.setExecutorPipeline(child); + + PipelineExecutor executor = newExecutor(meta, data); + executor.setVariable("TEST3_PARAMETER2", "B2"); + + // Empty field cell (mapped from value2) + executor.passParametersToPipeline(Collections.singletonList("")); + + assertEquals("default2", child.getVariable("TEST3_PARAMETER2")); + assertNull(executor.getVariable("TEST3_PARAMETER2")); + } + + @Test + void passParametersToPipelineNonEmptyFieldKeepsMappedValue() throws HopException { + PipelineExecutorMeta meta = new PipelineExecutorMeta(); + meta.setDefault(); + meta.setInheritingAllVariables(true); + PipelineExecutorParameters param = new PipelineExecutorParameters(); + param.setVariable("TEST3_PARAMETER2"); + param.setField("value2"); + param.setInput(""); + meta.setParameters(new ArrayList<>(Collections.singletonList(param))); + + RowMeta inputRowMeta = new RowMeta(); + inputRowMeta.addValueMeta(new ValueMetaString("value2")); + + PipelineMeta childMeta = new PipelineMeta(); + childMeta.setName("child"); + childMeta.addParameterDefinition("TEST3_PARAMETER2", "default2", "with default"); + + LocalPipelineEngine child = new LocalPipelineEngine(childMeta); + child.copyParametersFromDefinitions(childMeta); + child.setVariable("TEST3_PARAMETER2", "stale"); + + PipelineExecutorData data = new PipelineExecutorData(); + data.setInputRowMeta(inputRowMeta); + data.setExecutorPipeline(child); + + PipelineExecutor executor = newExecutor(meta, data); + + executor.passParametersToPipeline(Collections.singletonList("B2")); + + assertEquals("B2", child.getVariable("TEST3_PARAMETER2")); + } + @Test void collectPipelineResultsDoesNotForwardRowsWhenNoResultRowsTarget() throws HopException { PipelineExecutorMeta meta = new PipelineExecutorMeta(); diff --git a/plugins/transforms/textfile/src/main/java/org/apache/hop/pipeline/transforms/fileinput/text/TextFileInputDialog.java b/plugins/transforms/textfile/src/main/java/org/apache/hop/pipeline/transforms/fileinput/text/TextFileInputDialog.java index a3a199fce11..4c0be1472fb 100644 --- a/plugins/transforms/textfile/src/main/java/org/apache/hop/pipeline/transforms/fileinput/text/TextFileInputDialog.java +++ b/plugins/transforms/textfile/src/main/java/org/apache/hop/pipeline/transforms/fileinput/text/TextFileInputDialog.java @@ -2385,7 +2385,7 @@ public void getData( wFields.optimizeTableView(); // Date locale - wDateLocale.setText(meta.getContent().getDateFormatLocale()); + wDateLocale.setText(Const.NVL(meta.getContent().getDateFormatLocale(), "")); wShortFileFieldName.setText( Const.NVL(meta.getAdditionalOutputFields().getShortFilenameField(), "")); diff --git a/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutor.java b/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutor.java index b30485e5662..33d16ec86ff 100644 --- a/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutor.java +++ b/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutor.java @@ -197,7 +197,9 @@ private void executeWorkflow() throws HopException { data.executorWorkflow = createWorkflow(data.executorWorkflowMeta, this); - data.executorWorkflow.initializeFrom(getPipeline()); + // Re-apply variables from this transform after factory create so nested execution-info + // locations see HOP_DATA / EXECUTIONS_INFORMATION_FOLDER (and any mapPartitions injects). + data.executorWorkflow.initializeFrom(this); data.executorWorkflow.setParentPipeline(getPipeline()); data.executorWorkflow.setLogLevel(getLogLevel()); data.executorWorkflow.setInternalHopVariables(); @@ -360,8 +362,11 @@ private void executeWorkflow() throws HopException { IWorkflowEngine createWorkflow( WorkflowMeta workflowMeta, ILoggingObject parentLogging) throws HopException { + // Use this transform as the variable source (same space as the parent pipeline, including + // Spark-injected HOP_DATA / EXECUTIONS_INFORMATION_FOLDER) so execution-info location paths + // resolve when the workflow is nested under Native Spark mapPartitions / DRIVER_ONLY. return WorkflowEngineFactory.createWorkflowEngine( - getPipeline(), + this, resolve(meta.getRunConfigurationName()), metadataProvider, workflowMeta, diff --git a/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutorMeta.java b/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutorMeta.java index f03da0b27dc..f6f3b330bb0 100644 --- a/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutorMeta.java +++ b/plugins/transforms/workflowexecutor/src/main/java/org/apache/hop/pipeline/transforms/workflowexecutor/WorkflowExecutorMeta.java @@ -669,6 +669,19 @@ public void searchInfoAndTargetTransforms(List transforms) { TransformMeta.findTransform(transforms, resultRowsTargetTransform); resultFilesTargetTransformMeta = TransformMeta.findTransform(transforms, resultFilesTargetTransform); + + // Rebind TARGET streams: getTransformIOMeta() may have been created before names were + // resolved, leaving Stream.transformMeta null (Beam/Spark multi-target discovery). + List targetStreams = getTransformIOMeta().getTargetStreams(); + if (targetStreams.size() > 0) { + targetStreams.get(0).setTransformMeta(executionResultTargetTransformMeta); + } + if (targetStreams.size() > 1) { + targetStreams.get(1).setTransformMeta(resultRowsTargetTransformMeta); + } + if (targetStreams.size() > 2) { + targetStreams.get(2).setTransformMeta(resultFilesTargetTransformMeta); + } } @Override diff --git a/pom.xml b/pom.xml index eb865cf2f0b..012c37c1d9f 100644 --- a/pom.xml +++ b/pom.xml @@ -481,6 +481,7 @@ .asf.yaml **/licenses/* **/target/** + **/.gitkeep **/*.svg **/*.json **/hop.pwd @@ -530,9 +531,10 @@ **/integration-tests/ssh/keys/it_rsa.pub - **/src/main/samples/**/*.txt **/src/main/samples/**/*.csv + **/spark-demo/**/*.csv + **/spark-demo/**/_SUCCESS docs/content/** diff --git a/tools/spark-client-pack/materialize-pack.sh b/tools/spark-client-pack/materialize-pack.sh index 608206421d5..0e4fed6ebac 100755 --- a/tools/spark-client-pack/materialize-pack.sh +++ b/tools/spark-client-pack/materialize-pack.sh @@ -24,12 +24,16 @@ # ./tools/spark-client-pack/materialize-pack.sh 3.4.4 /path/to/hop # SPARK_CLIENT_VERSIONS="3.3.4 3.4.4 3.5.8" ./tools/spark-client-pack/materialize-pack.sh # -# Then generate a matching fat jar: +# Then generate a matching fat jar (Beam Spark 3.x packs only): # HOP_SPARK_CLIENT_VERSION=3.4.4 ./hop-conf.sh --generate-fat-jar=/tmp/hop-spark-3.4.4.jar \ # --spark-client-version=3.4.4 # # And run Hop with the same pack on the driver classpath: # HOP_SPARK_CLIENT_VERSION=3.4.4 ./hop-run.sh ... +# +# Native Spark 4 engine fat jar (no pack to materialise; uses plugins/engines/spark/lib): +# ./hop-conf.sh --generate-fat-jar=/tmp/hop-native-spark4.jar --spark-client-version=native +# Do not use "native" as a pack version argument to this script. set -euo pipefail diff --git a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiCompositeWidgets.java b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiCompositeWidgets.java index ce7b08e2188..7f10ac2abad 100644 --- a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiCompositeWidgets.java +++ b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiCompositeWidgets.java @@ -79,6 +79,12 @@ public class GuiCompositeWidgets { @Getter @Setter private IGuiPluginCompositeButtonsListener compositeButtonsListener; + /** Parent composite of the last {@link #createCompositeWidgets} call (for button refresh). */ + private Composite widgetsParentComposite; + + /** Parent GUI element id of the last {@link #createCompositeWidgets} call. */ + private String widgetsParentGuiElementId; + public GuiCompositeWidgets(IVariables variables) { this(variables, 0); } @@ -130,6 +136,9 @@ public void createCompositeWidgets( return; } + this.widgetsParentComposite = parent; + this.widgetsParentGuiElementId = parentGuiElementId; + // Loop over the GUI elements, create and remember the widgets... // boolean useNewLayout = isConfigPlugin(sourceData.getClass()); @@ -220,7 +229,7 @@ private Control addCompositeWidgets( // Add the GUI element // switch (elementType) { - case TEXT, FILENAME, FOLDER: + case TEXT, FILENAME, FOLDER, MULTI_LINE_TEXT: control = getTextControl(parent, guiElements, props, lastControl, label, useNewLayout); break; case CHECKBOX: @@ -381,9 +390,28 @@ private Button getButtonControl( Object guiObject = methodClass.getDeclaredConstructor().newInstance(); - // Invoke the button method + // Invoke the button method (mutations apply to sourceObject) // buttonMethod.invoke(guiObject, sourceObject); + + // Re-bind form fields from the (possibly mutated) source object. Template-load and + // similar buttons open modal dialogs; refresh both immediately and on the next UI + // cycle so the parent form reliably shows the new values after the shell restores. + final Object mutatedSource = sourceObject; + final Button sourceButton = button; + final String widgetId = guiElements.getId(); + refreshWidgetsAfterButton(mutatedSource, sourceButton, widgetId); + if (button.getDisplay() != null && !button.getDisplay().isDisposed()) { + button + .getDisplay() + .asyncExec( + () -> { + if (sourceButton.isDisposed()) { + return; + } + refreshWidgetsAfterButton(mutatedSource, sourceButton, widgetId); + }); + } } catch (Exception e) { LogChannel.UI.logError( "Error invoking method " @@ -399,6 +427,29 @@ private Button getButtonControl( return button; } + /** + * After an annotated BUTTON method returns, push {@code sourceObject} field values back into the + * composite widgets and notify listeners. Safe to call more than once. + */ + private void refreshWidgetsAfterButton( + Object sourceObject, Button sourceButton, String widgetId) { + if (compositeButtonsListener != null) { + compositeButtonsListener.afterButtonPressed(sourceObject); + } + if (widgetsParentComposite != null + && widgetsParentGuiElementId != null + && !widgetsParentComposite.isDisposed()) { + setWidgetsContents(sourceObject, widgetsParentComposite, widgetsParentGuiElementId); + if (!widgetsParentComposite.isDisposed()) { + widgetsParentComposite.layout(true, true); + widgetsParentComposite.redraw(); + } + } + if (compositeWidgetsListener != null && sourceButton != null && !sourceButton.isDisposed()) { + compositeWidgetsListener.widgetModified(this, sourceButton, widgetId); + } + } + private Link getLinkControl( Composite parent, GuiElements guiElements, @@ -545,9 +596,17 @@ private Control getTextControl( break; } + boolean multiLine = guiElements.getType() == GuiElementType.MULTI_LINE_TEXT; + int style; + if (multiLine) { + // Multi-line does not use password masking. + style = SWT.BORDER | SWT.MULTI | SWT.LEFT | SWT.WRAP | SWT.V_SCROLL | SWT.H_SCROLL; + } else { + style = SWT.BORDER | SWT.SINGLE | SWT.LEFT; + } + if (guiElements.isVariablesEnabled()) { - int style = SWT.BORDER | SWT.SINGLE | SWT.LEFT; - if (guiElements.isPassword()) { + if (!multiLine && guiElements.isPassword()) { String toolTip = StringUtils.isNotEmpty(guiElements.getToolTip()) ? guiElements.getToolTip() : null; // PasswordTextVar never mirrors the field value in the tooltip (TextVar may on some @@ -567,8 +626,7 @@ private Control getTextControl( text = textVar.getTextWidget(); } } else { - int style = SWT.BORDER | SWT.SINGLE | SWT.LEFT; - if (guiElements.isPassword()) { + if (!multiLine && guiElements.isPassword()) { style |= SWT.PASSWORD; } text = new Text(parent, style); @@ -581,6 +639,10 @@ private Control getTextControl( layoutControlBetweenLabelAndRightControl( props, lastControl, label, control, actionControl, useNewLayout); + if (multiLine) { + applyMultiLineTextHeight(props, control, text, guiElements.getMultiLineTextHeight()); + } + // Add an action based on the sub-type: switch (guiElements.getType()) { case FILENAME: @@ -648,6 +710,37 @@ public ITypeFilename instantiateTypeFilename(GuiElements guiElements) { } } + /** + * Sets an explicit FormData height for multi-line text so FormLayout does not collapse the + * control to a single line. + * + * @param props UI properties (zoom) + * @param control the outer control (may be TextVar composite) + * @param text the inner SWT Text + * @param lines preferred height in text lines + */ + private void applyMultiLineTextHeight(PropsUi props, Control control, Text text, int lines) { + int lineCount = Math.max(1, lines); + int lineHeight = 0; + try { + if (text != null && !text.isDisposed()) { + lineHeight = text.getLineHeight(); + } + } catch (Exception e) { + // Fall through to font-based estimate + } + if (lineHeight <= 0) { + // Typical default font height * zoom when the control is not yet realized + lineHeight = (int) Math.ceil(15 * props.getZoomFactor()); + } + // Small padding per line for borders / inter-line spacing + int linePx = lineHeight + Math.max(1, PropsUi.getMargin() / 2); + Object layoutData = control.getLayoutData(); + if (layoutData instanceof FormData fdControl) { + fdControl.height = lineCount * linePx; + } + } + private void layoutControlOnRight( Control lastControl, Control control, Label label, boolean useNewLayout) { FormData fdControl = new FormData(); @@ -826,6 +919,11 @@ public void setWidgetsContents( GuiElements guiElements = registry.findGuiElements(sourceData.getClass().getName(), parentGuiElementId); if (guiElements == null) { + LogChannel.UI.logError( + "setWidgetsContents: no GUI elements found for class: " + + sourceData.getClass().getName() + + CONST_PARENT_ID + + parentGuiElementId); return; } @@ -835,7 +933,33 @@ public void setWidgetsContents( compositeWidgetsListener.widgetsCreated(this); } - parentComposite.layout(true, true); + if (parentComposite != null && !parentComposite.isDisposed()) { + parentComposite.layout(true, true); + } + } + + /** + * Read a field value from the source object using the annotated getter method name when + * available, falling back to {@link PropertyDescriptor}. + */ + private Object readFieldValue(Object sourceData, GuiElements guiElements) { + try { + if (StringUtils.isNotEmpty(guiElements.getGetterMethod())) { + Method getter = sourceData.getClass().getMethod(guiElements.getGetterMethod()); + return getter.invoke(sourceData); + } + } catch (Exception e) { + // Fall through to PropertyDescriptor + } + try { + return new PropertyDescriptor(guiElements.getFieldName(), sourceData.getClass()) + .getReadMethod() + .invoke(sourceData); + } catch (Exception e) { + LogChannel.UI.logError( + "Unable to get value for field: '" + guiElements.getFieldName() + "'", e); + return null; + } } private void setWidgetsData(Object sourceData, GuiElements guiElements) { @@ -865,20 +989,11 @@ private void setWidgetsData(Object sourceData, GuiElements guiElements) { // What's the value? // - Object value = null; - try { - value = - new PropertyDescriptor(guiElements.getFieldName(), sourceData.getClass()) - .getReadMethod() - .invoke(sourceData); - } catch (Exception e) { - LogChannel.UI.logError( - "Unable to get value for field: '" + guiElements.getFieldName() + "'", e); - } + Object value = readFieldValue(sourceData, guiElements); String stringValue = value == null ? "" : Const.NVL(value.toString(), ""); switch (guiElements.getType()) { - case TEXT, FILENAME, FOLDER: + case TEXT, FILENAME, FOLDER, MULTI_LINE_TEXT: if (guiElements.isVariablesEnabled()) { TextVar textVar = (TextVar) control; textVar.setText(stringValue); @@ -991,7 +1106,7 @@ private void getWidgetsData(Object sourceData, GuiElements guiElements) { Object value = null; switch (guiElements.getType()) { - case TEXT, FILENAME, FOLDER: + case TEXT, FILENAME, FOLDER, MULTI_LINE_TEXT: if (guiElements.isVariablesEnabled()) { TextVar textVar = (TextVar) control; value = textVar.getText(); diff --git a/ui/src/main/java/org/apache/hop/ui/core/gui/IGuiPluginCompositeButtonsListener.java b/ui/src/main/java/org/apache/hop/ui/core/gui/IGuiPluginCompositeButtonsListener.java index d52fe18cabf..170f37e47b6 100644 --- a/ui/src/main/java/org/apache/hop/ui/core/gui/IGuiPluginCompositeButtonsListener.java +++ b/ui/src/main/java/org/apache/hop/ui/core/gui/IGuiPluginCompositeButtonsListener.java @@ -19,9 +19,20 @@ public interface IGuiPluginCompositeButtonsListener { /** - * This method is called when one of the buttons is pressed + * Called when a composite button is pressed, before the annotated button method + * runs. Typical use: flush widget contents into {@code sourceObject}. * - * @param sourceObject The source object + * @param sourceObject The source object behind the composite widgets */ void buttonPressed(Object sourceObject); + + /** + * Called after the annotated button method returns successfully. Typical use: re-bind widgets + * from {@code sourceObject} when the button mutated it (e.g. load template). + * + * @param sourceObject The source object behind the composite widgets + */ + default void afterButtonPressed(Object sourceObject) { + // optional + } } diff --git a/ui/src/main/java/org/apache/hop/ui/pipeline/config/PipelineRunConfigurationEditor.java b/ui/src/main/java/org/apache/hop/ui/pipeline/config/PipelineRunConfigurationEditor.java index 0fd24e336be..d6bf3eb7f78 100644 --- a/ui/src/main/java/org/apache/hop/ui/pipeline/config/PipelineRunConfigurationEditor.java +++ b/ui/src/main/java/org/apache/hop/ui/pipeline/config/PipelineRunConfigurationEditor.java @@ -460,6 +460,42 @@ public void widgetModified( setChanged(); } }); + // Flush form → model before button methods; re-bind model → form after mutations + // (e.g. Load configuration template). Do not call setWidgetsContent() here: that sets + // wPluginType and can fire changeConnectionType, which re-reads the still-stale widgets + // and overwrites the template values just applied to the model. + guiCompositeWidgets.setCompositeButtonsListener( + new org.apache.hop.ui.core.gui.IGuiPluginCompositeButtonsListener() { + @Override + public void buttonPressed(Object sourceObject) { + if (sourceObject != null) { + guiCompositeWidgets.getWidgetsContents( + sourceObject, PipelineRunConfiguration.GUI_PLUGIN_ELEMENT_PARENT_ID); + } + } + + @Override + public void afterButtonPressed(Object sourceObject) { + IPipelineEngineRunConfiguration engine = + sourceObject instanceof IPipelineEngineRunConfiguration + ? (IPipelineEngineRunConfiguration) sourceObject + : workingConfiguration.getEngineRunConfiguration(); + if (engine != null) { + workingConfiguration.setEngineRunConfiguration(engine); + } + if (engine != null + && guiCompositeWidgets != null + && wPluginSpecificComp != null + && !wPluginSpecificComp.isDisposed()) { + guiCompositeWidgets.setWidgetsContents( + engine, + wPluginSpecificComp, + PipelineRunConfiguration.GUI_PLUGIN_ELEMENT_PARENT_ID); + wPluginSpecificComp.layout(true, true); + } + setChanged(); + } + }); } } @@ -528,14 +564,26 @@ public void setWidgetsContent() { wExecutionInfoLocation.setText( Const.NVL(workingConfiguration.getExecutionInfoLocationName(), "")); if (workingConfiguration.getEngineRunConfiguration() != null) { - wPluginType.setText( - Const.NVL(workingConfiguration.getEngineRunConfiguration().getEnginePluginName(), "")); - guiCompositeWidgets.setWidgetsContents( - workingConfiguration.getEngineRunConfiguration(), - wPluginSpecificComp, - PipelineRunConfiguration.GUI_PLUGIN_ELEMENT_PARENT_ID); + // Only update the engine-type combo when the value actually changes. Unconditional + // setText fires SWT.Modify → changeConnectionType, which re-reads plugin widgets and + // can wipe in-memory mutations (e.g. after Load configuration template). + String pluginName = + Const.NVL(workingConfiguration.getEngineRunConfiguration().getEnginePluginName(), ""); + if (!pluginName.equals(Const.NVL(wPluginType.getText(), ""))) { + wPluginType.setText(pluginName); + } + if (guiCompositeWidgets != null + && wPluginSpecificComp != null + && !wPluginSpecificComp.isDisposed()) { + guiCompositeWidgets.setWidgetsContents( + workingConfiguration.getEngineRunConfiguration(), + wPluginSpecificComp, + PipelineRunConfiguration.GUI_PLUGIN_ELEMENT_PARENT_ID); + } } else { - wPluginType.setText(""); + if (!wPluginType.getText().isEmpty()) { + wPluginType.setText(""); + } } try {