|
| 1 | +--- |
| 2 | +sidebar_position: 3 |
| 3 | +title: "Run an XTable sync on Apache Spark" |
| 4 | +--- |
| 5 | + |
| 6 | +import Tabs from '@theme/Tabs'; |
| 7 | +import TabItem from '@theme/TabItem'; |
| 8 | + |
| 9 | +# Run an XTable sync on Apache Spark |
| 10 | + |
| 11 | +`xtable-spark-runtime` is a runtime jar that runs an Apache XTable™ (Incubating) sync on an Apache |
| 12 | +Spark cluster. As with any XTable sync, no data files are rewritten — the sync reads the source |
| 13 | +table's metadata and writes the target format's metadata alongside the data that is already there. |
| 14 | + |
| 15 | +The jar is meant to be dropped into a Spark job you already run, so you don't need a separate |
| 16 | +process or a separate cluster to keep a table interoperable across formats. |
| 17 | + |
| 18 | +## Getting the jar |
| 19 | + |
| 20 | +The jar is published to Maven Central as |
| 21 | +`org.apache.xtable:xtable-spark-runtime_2.12:0.4.0-incubating`. Download it once: |
| 22 | + |
| 23 | +```shell md title="shell" |
| 24 | +curl -O https://repo1.maven.org/maven2/org/apache/xtable/xtable-spark-runtime_2.12/0.4.0-incubating/xtable-spark-runtime_2.12-0.4.0-incubating.jar |
| 25 | +``` |
| 26 | + |
| 27 | +Every engine dependency is `provided`, so the jar is about 4 MB and reuses the Hudi, Iceberg and |
| 28 | +Delta libraries your cluster already has. See the [Downloads](/releases/downloads) page for the |
| 29 | +full list of releases. |
| 30 | + |
| 31 | +## Adding the sync to a Spark job |
| 32 | + |
| 33 | +This is what the jar is for. Your job already writes a table in one format, and you want that same |
| 34 | +table to be interoperable with the others as soon as the write finishes. |
| 35 | + |
| 36 | +Add the jar to the `spark-submit` you already use, alongside your application jar: |
| 37 | + |
| 38 | +```shell md title="shell" |
| 39 | +$SPARK_HOME/bin/spark-submit \ |
| 40 | + --jars xtable-spark-runtime_2.12-0.4.0-incubating.jar \ |
| 41 | + --class com.example.OrdersJob \ |
| 42 | + orders-job.jar |
| 43 | +``` |
| 44 | + |
| 45 | +Then call `XTableSyncService` after your write. You describe the table with a `TableSyncSpec` and |
| 46 | +hand it the session's Hadoop configuration: |
| 47 | + |
| 48 | +<Tabs |
| 49 | +groupId="language" |
| 50 | +defaultValue="scala" |
| 51 | +values={[ |
| 52 | +{ label: 'Scala', value: 'scala', }, |
| 53 | +{ label: 'Java', value: 'java', }, |
| 54 | +]} |
| 55 | +> |
| 56 | +<TabItem value="scala"> |
| 57 | +
|
| 58 | +```scala md title="OrdersJob.scala" |
| 59 | +import java.util.{Arrays => JArrays} |
| 60 | +import org.apache.xtable.spark.{TableSyncSpec, XTableSyncService} |
| 61 | + |
| 62 | +val basePath = "s3://example-warehouse/db/orders" |
| 63 | + |
| 64 | +// the write your job already does |
| 65 | +df.write.format("hudi").options(hudiOptions).mode("append").save(basePath) |
| 66 | + |
| 67 | +// the one call you add |
| 68 | +new XTableSyncService().sync( |
| 69 | + TableSyncSpec.builder() |
| 70 | + .key("orders") |
| 71 | + .basePath(basePath) |
| 72 | + .sourceFormat("HUDI") |
| 73 | + .targets(JArrays.asList("ICEBERG", "DELTA")) |
| 74 | + .build(), |
| 75 | + spark.sparkContext.hadoopConfiguration) |
| 76 | +``` |
| 77 | + |
| 78 | +</TabItem> |
| 79 | +<TabItem value="java"> |
| 80 | + |
| 81 | +```java md title="OrdersJob.java" |
| 82 | +import java.util.Arrays; |
| 83 | +import org.apache.xtable.spark.TableSyncSpec; |
| 84 | +import org.apache.xtable.spark.XTableSyncService; |
| 85 | + |
| 86 | +String basePath = "s3://example-warehouse/db/orders"; |
| 87 | + |
| 88 | +// the write your job already does |
| 89 | +df.write().format("hudi").options(hudiOptions).mode("append").save(basePath); |
| 90 | + |
| 91 | +// the one call you add |
| 92 | +new XTableSyncService() |
| 93 | + .sync( |
| 94 | + TableSyncSpec.builder() |
| 95 | + .key("orders") |
| 96 | + .basePath(basePath) |
| 97 | + .sourceFormat("HUDI") |
| 98 | + .targets(Arrays.asList("ICEBERG", "DELTA")) |
| 99 | + .build(), |
| 100 | + spark.sparkContext().hadoopConfiguration()); |
| 101 | +``` |
| 102 | + |
| 103 | +</TabItem> |
| 104 | +</Tabs> |
| 105 | + |
| 106 | +To compile against these classes, add the same Maven coordinates to your build with `provided` |
| 107 | +scope. |
| 108 | + |
| 109 | +The sync runs incrementally and tracks its own watermark in the target's sync metadata, falling |
| 110 | +back to a full snapshot whenever an incremental sync isn't safe — the first run, for example. That |
| 111 | +makes it safe to call after every write. |
| 112 | + |
| 113 | +:::note Tables are addressed by path |
| 114 | +The runtime jar identifies tables by path rather than through a catalog, so there is no equivalent |
| 115 | +of the `RunSync` Iceberg catalog config (`-i`) yet. For an Iceberg source, point `basePath` at the |
| 116 | +table root and set `dataPath` to `<basePath>/data`, since that is where Iceberg keeps its data |
| 117 | +files and where each target writes its metadata. |
| 118 | +::: |
| 119 | + |
| 120 | +:::tip Runnable example |
| 121 | +[`demo/spark-runtime`](https://github.com/apache/incubator-xtable/tree/main/demo/spark-runtime) is |
| 122 | +a complete job that syncs both directions and verifies the row counts. |
| 123 | +::: |
| 124 | + |
| 125 | +## Running a sync as its own job |
| 126 | + |
| 127 | +The jar also ships a `spark-submit` entry point, which is the equivalent of `RunSync` for this |
| 128 | +bundle. Pass the jar as the application jar rather than with `--jars`: |
| 129 | + |
| 130 | +```shell md title="shell" |
| 131 | +$SPARK_HOME/bin/spark-submit \ |
| 132 | + --class org.apache.xtable.spark.XTableSparkSync \ |
| 133 | + --master 'local[*]' \ |
| 134 | + xtable-spark-runtime_2.12-0.4.0-incubating.jar \ |
| 135 | + --basepath /path/to/hudi_table \ |
| 136 | + --sourceformat HUDI \ |
| 137 | + --targets ICEBERG,DELTA |
| 138 | +``` |
| 139 | + |
| 140 | +To sync several tables in one submit, use `--datasetconfig`. It takes the same YAML that `RunSync` |
| 141 | +uses, so an existing config works unchanged, and unlike `RunSync` the config itself may live on |
| 142 | +cloud storage: |
| 143 | + |
| 144 | +```yaml md title="my_config.yaml" |
| 145 | +sourceFormat: HUDI |
| 146 | +targetFormats: |
| 147 | + - DELTA |
| 148 | + - ICEBERG |
| 149 | +datasets: |
| 150 | + - |
| 151 | + tableBasePath: s3://tpc-ds-datasets/1GB/hudi/call_center |
| 152 | + tableDataPath: s3://tpc-ds-datasets/1GB/hudi/call_center/data |
| 153 | + tableName: call_center |
| 154 | + namespace: my.db |
| 155 | + - |
| 156 | + tableBasePath: s3://tpc-ds-datasets/1GB/hudi/catalog_sales |
| 157 | + tableName: catalog_sales |
| 158 | + partitionSpec: cs_sold_date_sk:VALUE |
| 159 | + - |
| 160 | + tableBasePath: s3://hudi/multi-partition-dataset |
| 161 | + tableName: multi_partition_dataset |
| 162 | + partitionSpec: time_millis:DAY:yyyy-MM-dd,type:VALUE |
| 163 | +``` |
| 164 | +
|
| 165 | +```shell md title="shell" |
| 166 | +$SPARK_HOME/bin/spark-submit \ |
| 167 | + --class org.apache.xtable.spark.XTableSparkSync \ |
| 168 | + xtable-spark-runtime_2.12-0.4.0-incubating.jar \ |
| 169 | + --datasetconfig my_config.yaml |
| 170 | +``` |
| 171 | + |
| 172 | +`--datasetconfig` and `--basepath` are mutually exclusive — pass one or the other. |
| 173 | + |
| 174 | +| Option | Description | |
| 175 | +| --- | --- | |
| 176 | +| `--basepath` | Base path of the source table. | |
| 177 | +| `--sourceformat` | Source format: `HUDI`, `ICEBERG`, `DELTA`, `PAIMON` or `PARQUET`. | |
| 178 | +| `--targets` | Comma-separated target formats, for example `ICEBERG,DELTA`. | |
| 179 | +| `--datasetconfig` | Path to a YAML config listing several tables. May be local or on cloud storage. | |
| 180 | +| `--datapath` | Path to the data files, when it differs from the base path. | |
| 181 | +| `--tablename` | Table name. Defaults to the last segment of the base path. | |
| 182 | +| `--namespace` | Dot-separated table namespace. | |
| 183 | +| `--partitionspec` | Hudi source partition field spec, for example `level:VALUE`. | |
| 184 | +| `--usedeltakernel` | Force Delta Kernel for a Delta source or target. | |
| 185 | +| `--help` | Print the usage text. | |
| 186 | + |
| 187 | +## Supported formats |
| 188 | + |
| 189 | +Paimon and Parquet are read-only sources; XTable does not write either format as a target. |
| 190 | + |
| 191 | +| Source ↓ / Target → | Hudi | Iceberg | Delta | |
| 192 | +| --- | :---: | :---: | :---: | |
| 193 | +| **Hudi** | – | ✅ | ✅ | |
| 194 | +| **Iceberg** | ✅ | – | ✅ | |
| 195 | +| **Delta** | ✅ | ✅ | – | |
| 196 | +| **Paimon** | ✅ | ✅ | ✅ | |
| 197 | +| **Parquet** | ✅ | ✅ | ✅ | |
| 198 | + |
| 199 | +## Spark version support |
| 200 | + |
| 201 | +Converting to and from Hudi and Iceberg doesn't require Spark at all, so those run on any of the |
| 202 | +Spark versions below. Delta is the only engine whose implementation depends on the Spark version, |
| 203 | +and the jar chooses the right one automatically: |
| 204 | + |
| 205 | +| Spark version | Hudi and Iceberg | Delta implementation | |
| 206 | +| --- | :---: | --- | |
| 207 | +| 3.4.x | ✅ | Delta Standalone | |
| 208 | +| 3.5.x and newer | ✅ | [Delta Kernel](https://docs.delta.io/latest/delta-kernel.html), selected automatically | |
| 209 | + |
| 210 | +Delta Standalone doesn't run on Spark 3.5, so on 3.5 and newer a Delta source or target is routed |
| 211 | +through Delta Kernel with no flag needed. If you want Kernel on Spark 3.4 as well, pass |
| 212 | +`--usedeltakernel`, or set `.useDeltaKernel(true)` on the `TableSyncSpec`. |
| 213 | + |
| 214 | +## Next steps |
| 215 | + |
| 216 | +- See the [Quickstart](/docs/how-to) for an end-to-end interoperability walkthrough. |
| 217 | +- See [Apache Spark](/docs/spark) for the options each format needs when you query a synced table. |
| 218 | +- See [Installation](/docs/setup) if you'd rather build the project from source. |
0 commit comments