diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index c20bf249..3eea9fc0 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -21,7 +21,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- java_version: ['11', '17', '21']
+ java_version: ['17', '21', '25']
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
@@ -44,27 +44,31 @@ jobs:
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
- files: ./dbeam-core/target/site/jacoco/jacoco.xml
+ files: ./dbeam-core/target/site/jacoco/jacoco.xml,./dbeam-parquet/target/site/jacoco/jacoco.xml
flags: unittests # optional
name: codecov-umbrella # optional
fail_ci_if_error: false # optional (default = false)
verbose: true # optional (default = false)
- - name: Install avro tools
- if: matrix.java_version == '17'
+ - name: Install avro and parquet tools
+ if: matrix.java_version == '21'
run: |
- wget https://dlcdn.apache.org/avro/avro-1.11.3/java/avro-tools-1.11.3.jar
+ wget https://dlcdn.apache.org/avro/avro-1.11.5/java/avro-tools-1.11.5.jar
mkdir -p /opt/avro
- mv avro-tools-1.11.3.jar /opt/avro/
- chmod +x /opt/avro/avro-tools-1.11.3.jar
- echo "alias avro-tools='java -jar /opt/avro/avro-tools-1.11.3.jar'" > ~/.bashrc
+ mv avro-tools-1.11.5.jar /opt/avro/
+ chmod +x /opt/avro/avro-tools-1.11.5.jar
+ echo "alias avro-tools='java -jar /opt/avro/avro-tools-1.11.5.jar'" > ~/.bashrc
+ wget -q https://repo1.maven.org/maven2/org/apache/parquet/parquet-cli/1.14.4/parquet-cli-1.14.4-runtime.jar
+ mkdir -p /opt/parquet
+ mv parquet-cli-1.14.4-runtime.jar /opt/parquet/
+ echo "alias parquet-tools='java -jar /opt/parquet/parquet-cli-1.14.4-runtime.jar'" >> ~/.bashrc
- name: End to end tests
shell: bash
- if: matrix.java_version == '17'
+ if: matrix.java_version == '21'
run: ./e2e/e2e.sh
- run: mkdir staging && cp /tmp/debeam_e2e.log ./staging
- if: matrix.java_version == '17'
+ if: matrix.java_version == '21'
- uses: actions/upload-artifact@v4
- if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main') && matrix.java_version == '17'
+ if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main') && matrix.java_version == '21'
with:
name: Package
path: staging
@@ -86,12 +90,12 @@ jobs:
uses: actions/cache@v4
with:
path: ~/.m2
- key: Linux-java11-m2-${{ hashFiles('**/pom.xml') }}
- restore-keys: Linux-java11-m2
- - name: Set up JDK 11
+ key: Linux-java21-m2-${{ hashFiles('**/pom.xml') }}
+ restore-keys: Linux-java21-m2
+ - name: Set up JDK 21
uses: actions/setup-java@v4
with:
- java-version: '11'
+ java-version: '21'
distribution: 'adopt' # use Corretto once supported https://github.com/actions/setup-java/issues/68
- name: Deploy
env:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 2ec3169f..d41c40e4 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -23,12 +23,12 @@ jobs:
uses: actions/cache@v4
with:
path: ~/.m2
- key: Linux-java11-m2-${{ hashFiles('**/pom.xml') }}
- restore-keys: Linux-java11-m2
- - name: Set up JDK 11
+ key: Linux-java17-m2-${{ hashFiles('**/pom.xml') }}
+ restore-keys: Linux-java17-m2
+ - name: Set up JDK 17
uses: actions/setup-java@v4
with:
- java-version: '11'
+ java-version: '17'
distribution: 'adopt'
# setup-java generates a settings.xml pointing deployments to Sonatype
# See https://github.com/actions/setup-java/blob/v3.11.0/docs/advanced-usage.md#publishing-using-apache-maven
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..0f7d5295
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,42 @@
+# Changelog
+
+## [Unreleased]
+
+### Added
+
+- **dbeam-parquet module**: New module for exporting SQL databases directly to Parquet files,
+ complementing the existing Avro export in dbeam-core.
+ - `JdbcParquetJob`: Main entry point for Parquet exports, reuses all shared infrastructure
+ from dbeam-core (JDBC connectors, query building, partitioning, parallel queries, Beam runners).
+ - `JdbcParquetSchema`: Converts JDBC ResultSetMetaData to Parquet MessageType schema with
+ full SQL type mapping including logical types (TIMESTAMP_MILLIS, UUID).
+ - `JdbcParquetWriteSupport`: Writes ResultSet rows directly to Parquet RecordConsumer
+ without intermediate objects.
+ - `ChannelOutputFile`: OutputFile implementation backed by WritableByteChannel, bypassing
+ Hadoop for file I/O.
+ - Parquet compression codecs: snappy, gzip, zstd, lz4, uncompressed via `--parquetCodec`
+ option (also auto-maps from `--avroCodec`).
+ - SQL ARRAY columns use Parquet 3-level LIST type with typed elements (INT32, INT64, FLOAT,
+ DOUBLE, BOOLEAN, STRING) inferred from column type name.
+ - Parquet file footer includes `parquet.avro.schema` key with the Avro schema JSON, generated
+ via JdbcAvroSchema for full compatibility with Spark, Hive, and BigQuery.
+ - `_AVRO_SCHEMA.avsc` file saved alongside `_PARQUET_SCHEMA.json` in output directory.
+ - `--parquetSchemaFilePath`: Input schema override (Parquet MessageType text format).
+ - `--rowGroupSize` (default 128MB) and `--pageSize` (default 1MB) options.
+ - `BeamJdbcParquetSchema`: Exposes schema creation timing as Beam metric.
+ - `PsqlParquetJob`: PostgreSQL-specific job with replication lag check.
+ - `BenchJdbcParquetJob`: Benchmarking job for multi-execution performance testing.
+ - End-to-end test support in `e2e/e2e.sh` with `parquet-tools` validation.
+ - Comprehensive test suite (90+ tests) covering schema conversion, record roundtrips,
+ typed arrays, null handling, mock-based PostgreSQL type tests, and footer metadata.
+
+### Changed
+
+- Parent POM: Shade plugin config parameterized via `${dbeam.mainClass}` property,
+ eliminating duplication between dbeam-core and dbeam-parquet.
+- `PsqlReplicationCheck.validateOptions()` made public for cross-module access.
+- dbeam-core: Added test-jar packaging for test helper sharing.
+- CI: Codecov upload includes dbeam-parquet coverage. Parquet-tools installed for e2e validation.
+- e2e: PostgreSQL upgraded from 16 to 18. Parquet test suite runs alongside Avro suite.
+- Documentation: README updated with Parquet usage examples, library dependencies, and feature list.
+ New `docs/parquet-type-conversion.md` with full type mapping table.
diff --git a/README.md b/README.md
index 8d659edd..5f2d9b47 100644
--- a/README.md
+++ b/README.md
@@ -17,28 +17,38 @@ This tool is runnable locally, or on any other backend supported by Apache Beam,
## Overview
DBeam is a tool that reads all the data from single SQL database table,
-converts the data into [Avro](https://avro.apache.org/) and stores it into
+converts the data into [Avro](https://avro.apache.org/) or [Parquet](https://parquet.apache.org/) and stores it into
appointed location, usually in GCS.
It runs as a single threaded [Apache Beam](https://beam.apache.org/) pipeline.
DBeam requires the database credentials, the database table name to read, and the output location
to store the extracted data into. DBeam first makes a single select into the target table with
limit one to infer the table schema. After the schema is created the job will be launched which
-simply streams the table contents via JDBC into target location as Avro.
+simply streams the table contents via JDBC into target location as Avro or Parquet.
-[Generated Avro Schema Type Conversion Details](docs/type-conversion.md)
+Type conversion details: [Avro](docs/type-conversion.md) | [Parquet](docs/parquet-type-conversion.md)
## dbeam-core package features
- Supports both PostgreSQL, MySQL, MariaDB, and H2 JDBC connectors
- Supports [Google CloudSQL](https://cloud.google.com/sql/) managed databases
-- Currently outputs only to Avro format
+- Outputs to Avro format
- Reads database from an external password file (`--passwordFile`) or an external [KMS](https://cloud.google.com/kms/) encrypted password file (`--passwordFileKmsEncrypted`)
- Can filter only records of the current day with the `--partitionColumn` parameter
- Check and fail on too old partition dates. Snapshot dumps are not filtered by a given date/partition, when running for a too old partition, the job fails to avoid new data in old partitions. (can be disabled with `--skipPartitionCheck`)
- Implemented as [Apache Beam SDK](https://beam.apache.org/) pipeline, supporting any of its [runners](https://beam.apache.org/documentation/runners/capability-matrix/) (tested with `DirectRunner` and `DataflowRunner`)
+## dbeam-parquet package features
+
+- Outputs to Parquet format (columnar, optimized for analytical queries)
+- Reuses all dbeam-core infrastructure (JDBC connectors, CloudSQL, partitioning, parallel queries)
+- Supports Parquet compression codecs: snappy, gzip, zstd, lz4, uncompressed
+- No full Hadoop dependency — uses a minimal `hadoop-common` for the Parquet API surface
+- Separate `--parquetCodec` option or automatic mapping from `--avroCodec`
+- Optional input schema file via `--parquetSchemaFilePath` (Parquet MessageType text format)
+- PostgreSQL replication check via `PsqlParquetJob`
+
### DBeam export parameters
```
@@ -171,15 +181,19 @@ Building and testing can be achieved with `mvn`:
mvn verify
```
-In order to create a jar with all dependencies under `./dbeam-core/target/dbeam-core-shaded.jar` run the following:
+In order to create jars with all dependencies run the following:
```sh
mvn clean package -Ppack
```
+This produces:
+- `./dbeam-core/target/dbeam-core-shaded.jar` — Avro export
+- `./dbeam-parquet/target/dbeam-parquet-shaded.jar` — Parquet export
+
## Usage examples
-Using Java from the command line:
+### Avro export
```sh
java -cp ./dbeam-core/target/dbeam-core-shaded.jar \
@@ -191,7 +205,20 @@ java -cp ./dbeam-core/target/dbeam-core-shaded.jar \
--table=my_table
```
-For CloudSQL:
+### Parquet export
+
+```sh
+java -cp ./dbeam-parquet/target/dbeam-parquet-shaded.jar \
+ com.spotify.dbeam.parquet.JdbcParquetJob \
+ --output=gs://my-testing-bucket-name/ \
+ --username=my_database_username \
+ --password=secret \
+ --connectionUrl=jdbc:postgresql://some.database.uri.example.org:5432/my_database \
+ --table=my_table \
+ --parquetCodec=snappy
+```
+
+For CloudSQL (Avro):
```sh
java -cp ./dbeam-core/target/dbeam-core-shaded.jar \
@@ -256,11 +283,19 @@ When using Google Cloud, [IAM authentication](https://github.com/GoogleCloudPlat
To include DBeam library in a mvn project add the following dependency in `pom.xml`:
```xml
+
com.spotify
dbeam-core
${dbeam.version}
+
+
+
+ com.spotify
+ dbeam-parquet
+ ${dbeam.version}
+
```
@@ -268,7 +303,8 @@ To include DBeam library in a SBT project add the following dependency in `build
```sbt
libraryDependencies ++= Seq(
- "com.spotify" % "dbeam-core" % dbeamVersion
+ "com.spotify" % "dbeam-core" % dbeamVersion, // Avro
+ "com.spotify" % "dbeam-parquet" % dbeamVersion // Parquet
)
```
diff --git a/dbeam-bom/pom.xml b/dbeam-bom/pom.xml
index 4e7d364e..f4435743 100644
--- a/dbeam-bom/pom.xml
+++ b/dbeam-bom/pom.xml
@@ -21,6 +21,11 @@
dbeam-core
${project.version}
+
+ com.spotify
+ dbeam-parquet
+ ${project.version}
+
@@ -47,7 +52,7 @@
org.codehaus.mojo
build-helper-maven-plugin
- 3.6.0
+ 3.6.1
attach-effective-pom
diff --git a/dbeam-core/pom.xml b/dbeam-core/pom.xml
index 6e2cd66e..c5fff8e3 100644
--- a/dbeam-core/pom.xml
+++ b/dbeam-core/pom.xml
@@ -13,6 +13,10 @@
DBeam Core
Top level DBeam core implementation
+
+ com.spotify.dbeam.jobs.JdbcAvroJob
+
+
@@ -93,8 +97,8 @@
- junit
- junit
+ org.junit.jupiter
+ junit-jupiter
test
@@ -124,4 +128,20 @@
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ test-jar
+
+
+
+
+
+
+
diff --git a/dbeam-core/src/main/java/com/spotify/dbeam/args/JdbcAvroArgs.java b/dbeam-core/src/main/java/com/spotify/dbeam/args/JdbcAvroArgs.java
index f3d04aa0..92e3c841 100644
--- a/dbeam-core/src/main/java/com/spotify/dbeam/args/JdbcAvroArgs.java
+++ b/dbeam-core/src/main/java/com/spotify/dbeam/args/JdbcAvroArgs.java
@@ -103,8 +103,8 @@ public static JdbcAvroArgs create(
}
public static JdbcAvroArgs create(final JdbcConnectionArgs jdbcConnectionArgs) {
- return create(jdbcConnectionArgs, 10000, "deflate6", Collections.emptyList(),
- "typed_first_row", false);
+ return create(
+ jdbcConnectionArgs, 10000, "deflate6", Collections.emptyList(), "typed_first_row", false);
}
public interface StatementPreparator extends Serializable {
diff --git a/dbeam-core/src/main/java/com/spotify/dbeam/avro/JdbcAvroIO.java b/dbeam-core/src/main/java/com/spotify/dbeam/avro/JdbcAvroIO.java
index 92e69b78..74d327a8 100644
--- a/dbeam-core/src/main/java/com/spotify/dbeam/avro/JdbcAvroIO.java
+++ b/dbeam-core/src/main/java/com/spotify/dbeam/avro/JdbcAvroIO.java
@@ -25,7 +25,6 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.io.CountingOutputStream;
import com.spotify.dbeam.args.JdbcAvroArgs;
-import com.spotify.dbeam.args.JdbcExportArgs;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.sql.Connection;
@@ -137,7 +136,7 @@ private static class JdbcAvroWriter extends FileBasedSink.Writer {
private final JdbcAvroArgs jdbcAvroArgs;
private DataFileWriter dataFileWriter;
private Connection connection;
- private JdbcAvroMetering metering;
+ private JdbcMetering metering;
private CountingOutputStream countingOutputStream;
JdbcAvroWriter(
@@ -147,7 +146,7 @@ private static class JdbcAvroWriter extends FileBasedSink.Writer {
super(writeOperation, MimeTypes.BINARY);
this.dynamicDestinations = dynamicDestinations;
this.jdbcAvroArgs = jdbcAvroArgs;
- this.metering = JdbcAvroMetering.create();
+ this.metering = JdbcMetering.create("jdbcavroio");
}
public Void getDestination() {
@@ -181,8 +180,7 @@ private ResultSet executeQuery(final String query) throws Exception {
jdbcAvroArgs.statementPreparator().setParameters(statement);
}
- if (jdbcAvroArgs.preCommand() != null
- && !jdbcAvroArgs.preCommand().isEmpty()) {
+ if (jdbcAvroArgs.preCommand() != null && !jdbcAvroArgs.preCommand().isEmpty()) {
final Statement stmt = connection.createStatement();
for (String command : jdbcAvroArgs.preCommand()) {
stmt.execute(command);
@@ -205,8 +203,9 @@ public void write(final String query) throws Exception {
LOGGER.info("jdbcavroio : Starting write...");
try (ResultSet resultSet = executeQuery(query)) {
metering.startWriteMeter();
- final JdbcAvroRecordConverter converter = JdbcAvroRecordConverter.create(resultSet,
- this.jdbcAvroArgs.arrayMode(), this.jdbcAvroArgs.nullableArrayItems());
+ final JdbcAvroRecordConverter converter =
+ JdbcAvroRecordConverter.create(
+ resultSet, this.jdbcAvroArgs.arrayMode(), this.jdbcAvroArgs.nullableArrayItems());
while (resultSet.next()) {
dataFileWriter.appendEncoded(converter.convertResultSetIntoAvroBytes());
this.metering.incrementRecordCount();
diff --git a/dbeam-core/src/main/java/com/spotify/dbeam/avro/JdbcAvroMetering.java b/dbeam-core/src/main/java/com/spotify/dbeam/avro/JdbcAvroMetering.java
index cb317775..c01752df 100644
--- a/dbeam-core/src/main/java/com/spotify/dbeam/avro/JdbcAvroMetering.java
+++ b/dbeam-core/src/main/java/com/spotify/dbeam/avro/JdbcAvroMetering.java
@@ -20,89 +20,17 @@
package com.spotify.dbeam.avro;
-import org.apache.beam.sdk.metrics.Counter;
-import org.apache.beam.sdk.metrics.Gauge;
-import org.apache.beam.sdk.metrics.Metrics;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class JdbcAvroMetering {
-
- public static final String RECORD_COUNT_METRIC_NAME = "recordCount";
- private final int countReportEvery;
- private final int logEvery;
- private final Logger logger = LoggerFactory.getLogger(JdbcAvroMetering.class);
- private Counter recordCount =
- Metrics.counter(this.getClass().getCanonicalName(), RECORD_COUNT_METRIC_NAME);
- private Counter executeQueryElapsedMs =
- Metrics.counter(this.getClass().getCanonicalName(), "executeQueryElapsedMs");
- private Counter writeElapsedMs =
- Metrics.counter(this.getClass().getCanonicalName(), "writeElapsedMs");
- private Gauge msPerMillionRows =
- Metrics.gauge(this.getClass().getCanonicalName(), "msPerMillionRows");
- private Gauge rowsPerMinute = Metrics.gauge(this.getClass().getCanonicalName(), "rowsPerMinute");
- private Counter bytesWritten =
- Metrics.counter(this.getClass().getCanonicalName(), "bytesWritten");
- private long rowCount = 0;
- private long writeIterateStartTime;
+/**
+ * @deprecated Use {@link JdbcMetering} directly instead.
+ */
+@Deprecated
+public class JdbcAvroMetering extends JdbcMetering {
public JdbcAvroMetering(int countReportEvery, int logEvery) {
- this.countReportEvery = countReportEvery;
- this.logEvery = logEvery;
+ super(countReportEvery, logEvery, "jdbcavroio");
}
public static JdbcAvroMetering create() {
return new JdbcAvroMetering(100000, 100000);
}
-
- /**
- * Increment and report counters to Beam SDK and logs. To avoid slowing down the writes, counts
- * are reported every x 1000s of rows. This exposes the job progress.
- */
- public void incrementRecordCount() {
- this.rowCount++;
- if ((this.rowCount % countReportEvery) == 0) {
- this.recordCount.inc(countReportEvery);
- final long elapsedNano = System.nanoTime() - this.writeIterateStartTime;
- final long msPerMillionRows = elapsedNano / rowCount;
- final long rowsPerMinute = (60 * 1000000000L) * rowCount / elapsedNano;
- this.msPerMillionRows.set(msPerMillionRows);
- this.rowsPerMinute.set(rowsPerMinute);
- if ((this.rowCount % logEvery) == 0) {
- logger.info(
- String.format(
- "jdbcavroio : Fetched # %08d rows at %08d rows per minute and %08d ms per M rows",
- rowCount, rowsPerMinute, msPerMillionRows));
- }
- }
- }
-
- public void exposeWriteElapsed() {
- long elapsedMs = (System.nanoTime() - this.writeIterateStartTime) / 1000000L;
- logger.info("jdbcavroio : Read {} rows, took {} seconds", rowCount, elapsedMs / 1000.0);
- this.writeElapsedMs.inc(elapsedMs);
- if (rowCount > 0) {
- this.recordCount.inc((this.rowCount % countReportEvery));
- this.msPerMillionRows.set(1000000L * elapsedMs / rowCount);
- if (elapsedMs != 0) {
- this.rowsPerMinute.set((60 * 1000L) * rowCount / elapsedMs);
- }
- }
- }
-
- public long startWriteMeter() {
- long startTs = System.nanoTime();
- this.writeIterateStartTime = startTs;
- this.rowCount = 0;
- return startTs;
- }
-
- public void exposeExecuteQueryMs(final long elapsedMs) {
- logger.info("jdbcavroio : Execute query took {} seconds", elapsedMs / 1000.0);
- this.executeQueryElapsedMs.inc(elapsedMs);
- }
-
- public void exposeWrittenBytes(final long count) {
- this.bytesWritten.inc(count);
- }
}
diff --git a/dbeam-core/src/main/java/com/spotify/dbeam/avro/JdbcAvroRecord.java b/dbeam-core/src/main/java/com/spotify/dbeam/avro/JdbcAvroRecord.java
index 082cb489..7abffd58 100644
--- a/dbeam-core/src/main/java/com/spotify/dbeam/avro/JdbcAvroRecord.java
+++ b/dbeam-core/src/main/java/com/spotify/dbeam/avro/JdbcAvroRecord.java
@@ -51,10 +51,8 @@ private static ByteBuffer nullableBytes(final byte[] bts) {
}
}
- static SqlFunction computeMapping(final ResultSetMetaData meta,
- final int column,
- final String arrayMode)
- throws SQLException {
+ static SqlFunction computeMapping(
+ final ResultSetMetaData meta, final int column, final String arrayMode) throws SQLException {
switch (meta.getColumnType(column)) {
case VARCHAR:
case CHAR:
diff --git a/dbeam-core/src/main/java/com/spotify/dbeam/avro/JdbcAvroRecordConverter.java b/dbeam-core/src/main/java/com/spotify/dbeam/avro/JdbcAvroRecordConverter.java
index 576d2b4a..9c96f593 100644
--- a/dbeam-core/src/main/java/com/spotify/dbeam/avro/JdbcAvroRecordConverter.java
+++ b/dbeam-core/src/main/java/com/spotify/dbeam/avro/JdbcAvroRecordConverter.java
@@ -49,9 +49,8 @@ public JdbcAvroRecordConverter(
this.nullableArrayItems = nullableArrayItems;
}
- public static JdbcAvroRecordConverter create(final ResultSet resultSet,
- final String arrayMode,
- final boolean nullableArrayItems)
+ public static JdbcAvroRecordConverter create(
+ final ResultSet resultSet, final String arrayMode, final boolean nullableArrayItems)
throws SQLException {
return new JdbcAvroRecordConverter(
computeAllMappings(resultSet, arrayMode),
@@ -62,8 +61,7 @@ public static JdbcAvroRecordConverter create(final ResultSet resultSet,
@SuppressWarnings("unchecked")
static JdbcAvroRecord.SqlFunction[] computeAllMappings(
- final ResultSet resultSet, final String arrayMode)
- throws SQLException {
+ final ResultSet resultSet, final String arrayMode) throws SQLException {
final ResultSetMetaData meta = resultSet.getMetaData();
final int columnCount = meta.getColumnCount();
@@ -151,8 +149,8 @@ private void writeValue(Object value, String column, BinaryEncoder binaryEncoder
} else {
if (arrayItem == null) {
throw new RuntimeException(
- String.format("Array item is null in column '%s', use --nullableArrayItems",
- column));
+ String.format(
+ "Array item is null in column '%s', use --nullableArrayItems", column));
}
writeValue(arrayItem, column, binaryEncoder);
@@ -162,8 +160,8 @@ private void writeValue(Object value, String column, BinaryEncoder binaryEncoder
binaryEncoder.writeArrayEnd();
} else {
throw new RuntimeException(
- String.format("Value of type %s in column '%s' is not supported", value.getClass(),
- column));
+ String.format(
+ "Value of type %s in column '%s' is not supported", value.getClass(), column));
}
}
}
diff --git a/dbeam-core/src/main/java/com/spotify/dbeam/avro/JdbcAvroSchema.java b/dbeam-core/src/main/java/com/spotify/dbeam/avro/JdbcAvroSchema.java
index 9d2eefee..39f3cec9 100644
--- a/dbeam-core/src/main/java/com/spotify/dbeam/avro/JdbcAvroSchema.java
+++ b/dbeam-core/src/main/java/com/spotify/dbeam/avro/JdbcAvroSchema.java
@@ -96,8 +96,12 @@ public static Schema createSchemaByReadingOneRow(
useLogicalTypes,
arrayMode,
nullableArrayItems);
- LOGGER.info("Schema created successfully. useLogicalTypes={}, arrayMode={}, "
- + "Generated schema: {}", useLogicalTypes, arrayMode, schema.toString());
+ LOGGER.info(
+ "Schema created successfully. useLogicalTypes={}, arrayMode={}, "
+ + "Generated schema: {}",
+ useLogicalTypes,
+ arrayMode,
+ schema.toString());
return schema;
}
}
@@ -181,9 +185,11 @@ private static SchemaBuilder.FieldAssembler createAvroFields(
fieldSchemaBuilder = field.type().unionOf().nullBuilder().endNull().and();
Array arrayInstance =
- resultSet.isFirst() && columnType == ARRAY
- && arrayMode.equals(ArrayHandlingMode.TypedMetaFromFirstRow)
- ? resultSet.getArray(i) : null;
+ resultSet.isFirst()
+ && columnType == ARRAY
+ && arrayMode.equals(ArrayHandlingMode.TypedMetaFromFirstRow)
+ ? resultSet.getArray(i)
+ : null;
final SchemaBuilder.UnionAccumulator> schemaFieldAssembler =
buildAvroFieldType(
@@ -216,17 +222,19 @@ private static SchemaBuilder.FieldAssembler createAvroFields(
*/
private static SchemaBuilder.UnionAccumulator>
buildAvroFieldType(
- final String columnName,
- final int columnType,
- final Array arrayInstance,
- final int precision,
- final String columnClassName,
- final String columnTypeName,
- final boolean useLogicalTypes,
- final String arrayMode,
- final boolean nullableArrayItems,
- final SchemaBuilder.BaseTypeBuilder>> field) throws SQLException {
+ final String columnName,
+ final int columnType,
+ final Array arrayInstance,
+ final int precision,
+ final String columnClassName,
+ final String columnTypeName,
+ final boolean useLogicalTypes,
+ final String arrayMode,
+ final boolean nullableArrayItems,
+ final SchemaBuilder.BaseTypeBuilder<
+ SchemaBuilder.UnionAccumulator>>
+ field)
+ throws SQLException {
switch (columnType) {
case BIGINT:
return field.longType();
@@ -265,19 +273,24 @@ private static SchemaBuilder.FieldAssembler createAvroFields(
if (arrayMode.equals(ArrayHandlingMode.TypedMetaPostgres)) {
if (!columnTypeName.startsWith("_")) {
- throw new RuntimeException("columnName=" + columnName
- + " columnTypeName=" + columnTypeName
- + " should start with '_'");
+ throw new RuntimeException(
+ "columnName="
+ + columnName
+ + " columnTypeName="
+ + columnTypeName
+ + " should start with '_'");
}
- return buildAvroFieldTypeFromPGType(columnName, columnTypeName.substring(1),
- useLogicalTypes, buildArrayItems(nullableArrayItems, field));
+ return buildAvroFieldTypeFromPGType(
+ columnName,
+ columnTypeName.substring(1),
+ useLogicalTypes,
+ buildArrayItems(nullableArrayItems, field));
}
if (arrayInstance == null) {
throw new RuntimeException(
- "When inspecting ARRAY column type in '" + columnName
- + "' its first value is NULL");
+ "When inspecting ARRAY column type in '" + columnName + "' its first value is NULL");
}
return buildAvroFieldType(
columnName,
@@ -324,12 +337,13 @@ private static SchemaBuilder.FieldAssembler createAvroFields(
}
}
- private static SchemaBuilder.BaseTypeBuilder>>
+ private static SchemaBuilder.BaseTypeBuilder<
+ SchemaBuilder.UnionAccumulator>>
buildArrayItems(
- final boolean nullableArrayItems,
- final SchemaBuilder.BaseTypeBuilder>> field) {
+ final boolean nullableArrayItems,
+ final SchemaBuilder.BaseTypeBuilder<
+ SchemaBuilder.UnionAccumulator>>
+ field) {
if (nullableArrayItems) {
return field.array().items().nullable();
} else {
@@ -339,12 +353,12 @@ private static SchemaBuilder.FieldAssembler createAvroFields(
private static SchemaBuilder.UnionAccumulator>
buildAvroFieldTypeFromPGType(
- final String columnName,
- final String columnTypeName,
- final boolean useLogicalTypes,
- final SchemaBuilder.BaseTypeBuilder<
- SchemaBuilder.UnionAccumulator>>
- field) {
+ final String columnName,
+ final String columnTypeName,
+ final boolean useLogicalTypes,
+ final SchemaBuilder.BaseTypeBuilder<
+ SchemaBuilder.UnionAccumulator>>
+ field) {
switch (columnTypeName) {
case "uuid":
if (useLogicalTypes) {
@@ -361,8 +375,14 @@ private static SchemaBuilder.FieldAssembler createAvroFields(
case "text":
return field.stringType();
default:
- throw new RuntimeException("columnName=" + columnName + " Postgres type '"
- + columnTypeName + "' is " + "not " + "supported");
+ throw new RuntimeException(
+ "columnName="
+ + columnName
+ + " Postgres type '"
+ + columnTypeName
+ + "' is "
+ + "not "
+ + "supported");
}
}
diff --git a/dbeam-core/src/main/java/com/spotify/dbeam/avro/JdbcMetering.java b/dbeam-core/src/main/java/com/spotify/dbeam/avro/JdbcMetering.java
new file mode 100644
index 00000000..cd337f9a
--- /dev/null
+++ b/dbeam-core/src/main/java/com/spotify/dbeam/avro/JdbcMetering.java
@@ -0,0 +1,110 @@
+/*-
+ * -\-\-
+ * DBeam Core
+ * --
+ * Copyright (C) 2016 - 2018 Spotify AB
+ * --
+ * Licensed 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 com.spotify.dbeam.avro;
+
+import org.apache.beam.sdk.metrics.Counter;
+import org.apache.beam.sdk.metrics.Gauge;
+import org.apache.beam.sdk.metrics.Metrics;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class JdbcMetering {
+
+ public static final String RECORD_COUNT_METRIC_NAME = "recordCount";
+ private final int countReportEvery;
+ private final int logEvery;
+ private final String formatName;
+ private final Logger logger = LoggerFactory.getLogger(JdbcMetering.class);
+ private Counter recordCount =
+ Metrics.counter(this.getClass().getCanonicalName(), RECORD_COUNT_METRIC_NAME);
+ private Counter executeQueryElapsedMs =
+ Metrics.counter(this.getClass().getCanonicalName(), "executeQueryElapsedMs");
+ private Counter writeElapsedMs =
+ Metrics.counter(this.getClass().getCanonicalName(), "writeElapsedMs");
+ private Gauge msPerMillionRows =
+ Metrics.gauge(this.getClass().getCanonicalName(), "msPerMillionRows");
+ private Gauge rowsPerMinute = Metrics.gauge(this.getClass().getCanonicalName(), "rowsPerMinute");
+ private Counter bytesWritten =
+ Metrics.counter(this.getClass().getCanonicalName(), "bytesWritten");
+ private long rowCount = 0;
+ private long writeIterateStartTime;
+
+ public JdbcMetering(int countReportEvery, int logEvery, String formatName) {
+ this.countReportEvery = countReportEvery;
+ this.logEvery = logEvery;
+ this.formatName = formatName;
+ }
+
+ public static JdbcMetering create(String formatName) {
+ return new JdbcMetering(100000, 100000, formatName);
+ }
+
+ /**
+ * Increment and report counters to Beam SDK and logs. To avoid slowing down the writes, counts
+ * are reported every x 1000s of rows. This exposes the job progress.
+ */
+ public void incrementRecordCount() {
+ this.rowCount++;
+ if ((this.rowCount % countReportEvery) == 0) {
+ this.recordCount.inc(countReportEvery);
+ final long elapsedNano = System.nanoTime() - this.writeIterateStartTime;
+ final long msPerMillionRows = elapsedNano / rowCount;
+ final long rowsPerMinute = (60 * 1000000000L) * rowCount / elapsedNano;
+ this.msPerMillionRows.set(msPerMillionRows);
+ this.rowsPerMinute.set(rowsPerMinute);
+ if ((this.rowCount % logEvery) == 0) {
+ logger.info(
+ String.format(
+ "%s : Fetched # %08d rows at %08d rows per minute and %08d ms per M rows",
+ formatName, rowCount, rowsPerMinute, msPerMillionRows));
+ }
+ }
+ }
+
+ public void exposeWriteElapsed() {
+ long elapsedMs = (System.nanoTime() - this.writeIterateStartTime) / 1000000L;
+ logger.info("{} : Read {} rows, took {} seconds", formatName, rowCount, elapsedMs / 1000.0);
+ this.writeElapsedMs.inc(elapsedMs);
+ if (rowCount > 0) {
+ this.recordCount.inc((this.rowCount % countReportEvery));
+ this.msPerMillionRows.set(1000000L * elapsedMs / rowCount);
+ if (elapsedMs != 0) {
+ this.rowsPerMinute.set((60 * 1000L) * rowCount / elapsedMs);
+ }
+ }
+ }
+
+ public long startWriteMeter() {
+ long startTs = System.nanoTime();
+ this.writeIterateStartTime = startTs;
+ this.rowCount = 0;
+ return startTs;
+ }
+
+ public void exposeExecuteQueryMs(final long elapsedMs) {
+ logger.info("{} : Execute query took {} seconds", formatName, elapsedMs / 1000.0);
+ this.executeQueryElapsedMs.inc(elapsedMs);
+ }
+
+ public void exposeWrittenBytes(final long count) {
+ this.bytesWritten.inc(count);
+ }
+}
diff --git a/dbeam-core/src/main/java/com/spotify/dbeam/jobs/BenchJdbcAvroJob.java b/dbeam-core/src/main/java/com/spotify/dbeam/jobs/BenchJdbcAvroJob.java
index 27265f64..269a6eca 100644
--- a/dbeam-core/src/main/java/com/spotify/dbeam/jobs/BenchJdbcAvroJob.java
+++ b/dbeam-core/src/main/java/com/spotify/dbeam/jobs/BenchJdbcAvroJob.java
@@ -20,112 +20,18 @@
package com.spotify.dbeam.jobs;
-import static com.google.common.collect.Lists.newArrayList;
-
-import com.google.common.collect.ImmutableMap;
-import com.google.common.math.Stats;
-import com.spotify.dbeam.beam.MetricsHelper;
-import com.spotify.dbeam.options.OutputOptions;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.function.Function;
-import java.util.stream.Collector;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-import java.util.stream.Stream;
-import org.apache.beam.sdk.PipelineResult;
-import org.apache.beam.sdk.options.Default;
-import org.apache.beam.sdk.options.Description;
-import org.apache.beam.sdk.options.PipelineOptions;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
/** Used on e2e test, allows benchmarking with different configuration parameters. */
public class BenchJdbcAvroJob {
- public interface BenchJdbcAvroOptions extends PipelineOptions {
- @Description("The JDBC connection url to perform the export.")
- @Default.Integer(3)
- int getExecutions();
-
- void setExecutions(int value);
- }
-
- private final PipelineOptions pipelineOptions;
- private List