Run Apify actor and update Notion
Run an Apify actor, pull its dataset, and publish the scraped results straight into a Notion page.
Find MySQL tables with no primary key, alert Slack, promote a unique NOT NULL key online or add an invisible id, and verify with a second audit.
A MySQL table without a primary key works until it meets replication:
UPDATE or DELETE on the source becomes a full table scan on the replica, row by row, and replica lag grows with the table.Legacy schemas, ETL staging tables and "temporary" tables that stayed are the usual cases.
This blueprint finds those tables, and MyISAM or other non-InnoDB tables, in the schemas you choose. It alerts Slack, and can give each InnoDB table a primary key, then audit again to verify. It uses the least disruptive fix available:
my_row_id column that MySQL 8.0.30's sql_generate_invisible_primary_key would have created.This blueprint was created by zkasuran.
| Finding | Severity | Fix |
|---|---|---|
NO_PRIMARY_KEY with a unique key on NOT NULL columns |
High | ADD PRIMARY KEY (cols), DROP INDEX uq, ALGORITHM=INPLACE, LOCK=NONE |
NO_PRIMARY_KEY with no such key, up to max_rows_for_id rows |
High | ADD COLUMN my_row_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT INVISIBLE PRIMARY KEY FIRST, LOCK=SHARED |
NO_PRIMARY_KEY above max_rows_for_id |
High | Report only, use an online schema change tool |
NON_INNODB_TABLE |
Medium | Report only, converting the engine is a migration |
A unique key qualifies only if it has no nullable column, no prefix and no expression. The smallest one wins. MySQL refuses LOCK=NONE when adding an auto-increment column, so that change blocks writes for the length of the rebuild. The flow says so, and max_rows_for_id keeps it to tables where that is acceptable.
audit (io.kestra.plugin.jdbc.mysql.Query, read-only) reads information_schema.tables, table_constraints, statistics and columns for the chosen schemas. It returns each finding with its candidate key and fix. Names are quoted with backticks, with embedded backticks doubled.summary logs every finding and the fix it will get.route (io.kestra.plugin.core.flow.If):alert posts to Slack.remediate_gate runs only when auto_remediate is true and dry_run is false.remediate (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) runs one ALTER TABLE at a time.reaudit and verify_result fail if a fixed table still has no primary key.errors alerts Slack when the audit cannot run.SELECT * does not return, so applications keep working, and a second audit as proof.information_schema query driving DDL.The query finds the tables once. Kestra runs it every week as new tables appear, sends findings to Slack, and applies the DDL behind auto_remediate and dry_run, one table at a time. It verifies the result and keeps a history of which table got which key.
On Kestra 2.0.5 OSS against MySQL 8.4, with a local HTTP sink standing in for Slack. Schema shop:
inventory: 8,000 rows, unique key on two NOT NULL columns;order_log: 5,000 rows, unique key with a nullable column;page_views: 30,000 rows, no key;weird`name: no key;sessions: MyISAM;products: has a primary key.| Run | Inputs | Result |
|---|---|---|
| 1 | dry run | 5 findings. inventory gets the promotion of uq_sku_wh. order_log's key is rejected because event is nullable. products not flagged |
| 2 | remediate, first version | verify_result failed on 3 tables: MySQL answered LOCK=NONE is not supported ... Adding an auto-increment column requires a lock. The invisible id now uses LOCK=SHARED and a row limit |
| 3 | remediate, max_rows_for_id: 20000 |
order_log and weird`name get my_row_id, page_views (30,000 rows) is held back. Verified. SELECT * on order_log still returns only its 3 original columns |
| 4 | remediate, default limit | page_views gets my_row_id, verified |
| 5 | dry run | Only the MyISAM table left, report only |
| 6 | unreachable server | errors alert sent |
SELECT on information_schema. For remediation, ALTER on the tables.docker run -d --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=pw mysql:8.4, then create tables without a PRIMARY KEY.MYSQL_USERNAME / MYSQL_PASSWORD: credentials used by every query.SLACK_WEBHOOK_URL: used by alert and the errors block.SECRET_.| Input | Default | Purpose |
|---|---|---|
jdbc_url |
jdbc:mysql://localhost:3306/mysql |
Server |
schemas |
[shop] |
Schemas to check |
add_invisible_id |
true |
Add my_row_id when no unique key fits |
max_rows_for_id |
1000000 |
Largest table that gets my_row_id |
auto_remediate |
false |
Allow ALTER TABLE |
dry_run |
true |
Must be false for a statement to run |
MYSQL_USERNAME, MYSQL_PASSWORD and SLACK_WEBHOOK_URL secrets.jdbc_url and schemas.auto_remediate: true and dry_run: false in a maintenance window, with max_rows_for_id set to what a write block of a few seconds allows.weekly trigger, which always runs in safe mode.max_rows_for_id to gh-ost or pt-online-schema-change from a task in the same flow.sql_generate_invisible_primary_key so new tables get a key automatically.ALTER and pause when it grows.