OutputValues icon
Query icon
If icon
IonToParquet icon
Upload icon
Download icon
Query icon
Assert icon
Set icon
Log icon
SlackIncomingWebhook icon
Schedule icon

Incremental MySQL to Parquet Export in MinIO with a Verified KV Watermark

Export changed MySQL rows to Parquet in MinIO with a (timestamp, key) watermark in KV that moves only after the object is read back.

Categories
Data

A cron script that moves a timestamp before the upload is confirmed loses rows when the upload fails. A plain timestamp cursor with a LIMIT also skips rows that share a timestamp. In this flow, the KV watermark moves last, after the Parquet object is read back from MinIO and checked.

How it works

  1. The Schedule trigger starts the flow every 15 minutes.
  2. read_watermark reads the (updated_at, id) cursor from KV. The first run uses start_from.
  3. plan_window finds the next window: at most max_rows rows, older than lag_seconds. has_changes (If) stops when it is empty.
  4. export_window streams the rows. to_parquet writes Snappy Parquet with an exact _cursor_ts column.
  5. upload_object writes <prefix>/<table>/dt=<day>/<table>_<first cursor>.parquet. A re-run of a window replaces the object.
  6. read_back and inspect_object (DuckDB) count rows, keys and rows outside the window. verify_object (Assert) compares them with the export.
  7. advance_watermark stores the new cursor in KV. On failure, alert_failure posts to Slack and the watermark stays.

What you get

  • At-least-once delivery: a failed run exports the same window again.
  • No skipped rows on timestamp ties.
  • Bounded runs. A WARN log shows that a backlog remains.
  • Object metadata and a KV record with the cursor range and execution id.

Who it's for

  • Data engineers who land MySQL changes in a lake without CDC.

Why orchestrate this with Kestra

The watermark lives in KV, not in a file on one server. Each task has its own retry, and concurrency stops two runs from using the same watermark.

Prerequisites

  • MySQL 8.0 or later, with a DATETIME(6) column set by ON UPDATE CURRENT_TIMESTAMP(6) and an index on (updated_at, id).
  • A bucket that exists. Hard deletes are not exported.
  • Parquet types come from IonToParquet: DECIMAL becomes a string and timestamps keep milliseconds.

Secrets

  • MYSQL_JDBC_URL, MYSQL_USERNAME, MYSQL_PASSWORD: connection and user with SELECT.
  • MINIO_ENDPOINT, MINIO_ACCESS_KEY_ID, MINIO_SECRET_KEY_ID: bucket access.
  • SLACK_WEBHOOK_URL: failure alerts.

Quick start

  1. Add the secrets and create the bucket.
  2. Execute the flow and read the log_result message.
  3. Check the mysql_parquet_watermark_<table> KV key. Delete it to export again from start_from.

How to extend

  • Give to_parquet an Avro schema to fix column types.
  • Start a downstream merge after advance_watermark.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.