Build an ETL Pipeline from a REST API Using Python, Polars, and DuckDB
Extract data from a REST API, reshape it with Python and Polars, then run analytical SQL on it with DuckDB, all in one Kestra flow.
Refresh stale persistent InnoDB row estimates after bulk loads with a bounded exact count, locked table identity checks and approved local ANALYZE.
READ locks block writers during maintenance. Choose a quiet window and one known bulk-load target.
Native MySQL Query reads one file-per-table InnoDB target. Count rows within a time budget then compare persistent n_rows with the exact count. Eligibility requires the row floor, stats age and abs(estimate-count)/count >= drift_ratio. Missing statistics or unsupported targets fail before maintenance. Live mode pauses for explicit approval. MySQL Queries takes a READ lock, checks TABLE_ID, count and physical size again then runs ANALYZE LOCAL TABLE on the same connection. An error status or a fresh estimate outside verify_tolerance fails the run. The final count and TABLE_ID must still match approval. Connection pooling is off; maintenance uses no transaction wrapper because MySQL implicitly commits these statements.
A bounded statistics refresh after a loader missed its final ANALYZE. InnoDB samples pages, so the estimate is checked with relative tolerance. This flow changes statistics, not application rows. LOCAL suppresses binary logging of maintenance. It makes no query-speed or plan-improvement claim.
MySQL operators managing tables with persistent statistics after bulk loads or purges, especially when STATS_AUTO_RECALC=0 requires explicit maintenance.
Connect the loader's target to a bounded count, approval and verified maintenance result. Slack reports stale estimates and failed runs. The disabled daily trigger forces read-only inputs. All SQL uses native MySQL Query or Queries; no client script or stored procedure is installed.
MySQL 8.0.46, Kestra 2.0.6 (MySQL plugin 2.3.0) and 2.0.5 (plugin 2.1.0). A second MySQL instance repeated recovery. Independent mysql reads preserved count, key sum, checksum and ordered data SHA-256. After loading 1,000 rows, n_rows changed from 2 to 1,002. EXPLAIN already estimated 1,002 before refresh.
| Case | Execution | Result |
|---|---|---|
| Healthy | 6q34wrazmW3P6l7Z2clW2j | SUCCESS |
| Dry run | iEdplWhRZpm6D8MSrYPNo | No write |
| Declined | 5d4U39kdb3Gyp7Fk4K4zXR | No write |
| Refresh | 2Pb2vF1ZQ6tCLEneNCfNLe | 2 to 1,002 |
| Repeat | 1Fg1uqc6FhoQeA4InSsdfJ | No write |
| Row changed | 7WdeXyRPsoxmzYoH5qHpfD | FAILED |
| Refused write | 5juaTVEWM42maGcTqYzgzB | FAILED |
MySQL 8.0.24 or later, unpartitioned file-per-table InnoDB with persistent statistics. Tested grants: SELECT and INSERT on the target, LOCK TABLES on its database, PROCESS and SELECT on mysql.innodb_table_stats. INSERT is required by ANALYZE. Use a JDBC URL without credentials and configure TLS for your server.
MYSQL_USERNAME, MYSQL_PASSWORD and SLACK_WEBHOOK_URL. Password and webhook overrides are SECRET inputs.
schema_name/table_name allow 1 to 64 ASCII letters, digits or underscores; system schemas are refused. max_table_mb defaults to 16 MiB (1 to 128). min_rows defaults to 1,000; min_stats_age_seconds to 3,600. drift_ratio defaults to 0.5 (0.1 to 1); verify_tolerance to 0.25 (0.01 to 0.5). count_timeout_ms defaults to 10,000 (1 to 10,000). dry_run=true and auto_remediate=false are safe defaults.
Trigger from the loader that owns this table. Limits: one target, no partitions or histograms. Metadata and row lock waits are 2 seconds; connect timeout is 5 seconds and socket timeout 30 seconds. Socket timeout does not guarantee server-side ANALYZE cancellation. Approval expires after one hour. Queue scope is this flow only. Stats age cannot be reread under LOCK TABLES, so already-fresh stats may be refreshed. Equal-count data edits are allowed; table replacement is refused. Recount after any failure before retrying.
This blueprint was created by zkasuran.
