pythia-mysql 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. pythia_mysql-0.1.0/LICENSE +21 -0
  2. pythia_mysql-0.1.0/PKG-INFO +425 -0
  3. pythia_mysql-0.1.0/README.md +397 -0
  4. pythia_mysql-0.1.0/pyproject.toml +45 -0
  5. pythia_mysql-0.1.0/setup.cfg +4 -0
  6. pythia_mysql-0.1.0/src/pythia_mysql/__init__.py +5 -0
  7. pythia_mysql-0.1.0/src/pythia_mysql/__main__.py +5 -0
  8. pythia_mysql-0.1.0/src/pythia_mysql/catalog.py +104 -0
  9. pythia_mysql-0.1.0/src/pythia_mysql/cli.py +8 -0
  10. pythia_mysql-0.1.0/src/pythia_mysql/engine.py +288 -0
  11. pythia_mysql-0.1.0/src/pythia_mysql/guard.py +63 -0
  12. pythia_mysql-0.1.0/src/pythia_mysql/queries/columns.sql +17 -0
  13. pythia_mysql-0.1.0/src/pythia_mysql/queries/current-schema.sql +4 -0
  14. pythia_mysql-0.1.0/src/pythia_mysql/queries/current-user.sql +5 -0
  15. pythia_mysql-0.1.0/src/pythia_mysql/queries/dependencies-catalog.sql +36 -0
  16. pythia_mysql-0.1.0/src/pythia_mysql/queries/dependencies-text.sql +16 -0
  17. pythia_mysql-0.1.0/src/pythia_mysql/queries/dependents-catalog.sql +32 -0
  18. pythia_mysql-0.1.0/src/pythia_mysql/queries/dependents-text.sql +21 -0
  19. pythia_mysql-0.1.0/src/pythia_mysql/queries/event.sql +23 -0
  20. pythia_mysql-0.1.0/src/pythia_mysql/queries/grants.sql +6 -0
  21. pythia_mysql-0.1.0/src/pythia_mysql/queries/grep.sql +24 -0
  22. pythia_mysql-0.1.0/src/pythia_mysql/queries/invalid-views.sql +12 -0
  23. pythia_mysql-0.1.0/src/pythia_mysql/queries/object-names.sql +28 -0
  24. pythia_mysql-0.1.0/src/pythia_mysql/queries/rehearsal-settings.sql +5 -0
  25. pythia_mysql-0.1.0/src/pythia_mysql/queries/resolve.sql +26 -0
  26. pythia_mysql-0.1.0/src/pythia_mysql/queries/rollback-to-savepoint.sql +5 -0
  27. pythia_mysql-0.1.0/src/pythia_mysql/queries/routine-params.sql +15 -0
  28. pythia_mysql-0.1.0/src/pythia_mysql/queries/routine.sql +25 -0
  29. pythia_mysql-0.1.0/src/pythia_mysql/queries/row-set-count.sql +9 -0
  30. pythia_mysql-0.1.0/src/pythia_mysql/queries/row-set-sample.sql +6 -0
  31. pythia_mysql-0.1.0/src/pythia_mysql/queries/savepoint.sql +4 -0
  32. pythia_mysql-0.1.0/src/pythia_mysql/queries/server-version.sql +6 -0
  33. pythia_mysql-0.1.0/src/pythia_mysql/queries/session-read-only.sql +6 -0
  34. pythia_mysql-0.1.0/src/pythia_mysql/queries/table-checks.sql +14 -0
  35. pythia_mysql-0.1.0/src/pythia_mysql/queries/table-columns.sql +18 -0
  36. pythia_mysql-0.1.0/src/pythia_mysql/queries/table-comment.sql +8 -0
  37. pythia_mysql-0.1.0/src/pythia_mysql/queries/table-engine.sql +11 -0
  38. pythia_mysql-0.1.0/src/pythia_mysql/queries/table-foreign-keys.sql +19 -0
  39. pythia_mysql-0.1.0/src/pythia_mysql/queries/table-head.sql +19 -0
  40. pythia_mysql-0.1.0/src/pythia_mysql/queries/table-indexes.sql +18 -0
  41. pythia_mysql-0.1.0/src/pythia_mysql/queries/triggers.sql +22 -0
  42. pythia_mysql-0.1.0/src/pythia_mysql/queries/unchecked.sql +17 -0
  43. pythia_mysql-0.1.0/src/pythia_mysql/queries/view-errors.sql +14 -0
  44. pythia_mysql-0.1.0/src/pythia_mysql/queries/view.sql +12 -0
  45. pythia_mysql-0.1.0/src/pythia_mysql/queries/views-reading.sql +8 -0
  46. pythia_mysql-0.1.0/src/pythia_mysql/queries/views-using-function.sql +8 -0
  47. pythia_mysql-0.1.0/src/pythia_mysql/queries/views-using.sql +8 -0
  48. pythia_mysql-0.1.0/src/pythia_mysql/queries/warnings.sql +5 -0
  49. pythia_mysql-0.1.0/src/pythia_mysql/read.py +630 -0
  50. pythia_mysql-0.1.0/src/pythia_mysql/rules.py +190 -0
  51. pythia_mysql-0.1.0/src/pythia_mysql/skills/pythia-mysql-apply/SKILL.md +115 -0
  52. pythia_mysql-0.1.0/src/pythia_mysql/skills/pythia-mysql-apply/reference/refusals.md +34 -0
  53. pythia_mysql-0.1.0/src/pythia_mysql/skills/pythia-mysql-autopilot/SKILL.md +65 -0
  54. pythia_mysql-0.1.0/src/pythia_mysql/skills/pythia-mysql-deploy/SKILL.md +73 -0
  55. pythia_mysql-0.1.0/src/pythia_mysql/skills/pythia-mysql-explore/SKILL.md +72 -0
  56. pythia_mysql-0.1.0/src/pythia_mysql/skills/pythia-mysql-impact/SKILL.md +62 -0
  57. pythia_mysql-0.1.0/src/pythia_mysql/skills/pythia-mysql-verify/SKILL.md +59 -0
  58. pythia_mysql-0.1.0/src/pythia_mysql/skills/using-pythia-mysql/SKILL.md +67 -0
  59. pythia_mysql-0.1.0/src/pythia_mysql/write.py +1087 -0
  60. pythia_mysql-0.1.0/src/pythia_mysql.egg-info/PKG-INFO +425 -0
  61. pythia_mysql-0.1.0/src/pythia_mysql.egg-info/SOURCES.txt +64 -0
  62. pythia_mysql-0.1.0/src/pythia_mysql.egg-info/dependency_links.txt +1 -0
  63. pythia_mysql-0.1.0/src/pythia_mysql.egg-info/entry_points.txt +2 -0
  64. pythia_mysql-0.1.0/src/pythia_mysql.egg-info/requires.txt +2 -0
  65. pythia_mysql-0.1.0/src/pythia_mysql.egg-info/top_level.txt +1 -0
  66. pythia_mysql-0.1.0/tests/test_mysql.py +1643 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 thaild
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,425 @@
1
+ Metadata-Version: 2.4
2
+ Name: pythia-mysql
3
+ Version: 0.1.0
4
+ Summary: pythia for MySQL: stored-program development for AI agents over pythia-core - catalog queries, impact analysis and an approved write path.
5
+ License-Expression: MIT
6
+ Project-URL: Repository, https://github.com/thaildhe172591/pythia-mysql
7
+ Project-URL: Changelog, https://github.com/thaildhe172591/pythia-mysql/blob/main/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/thaildhe172591/pythia-mysql/issues
9
+ Keywords: mysql,stored-procedures,database,agent-skills
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Database
21
+ Classifier: Topic :: Software Development
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: pythia-core<0.2,>=0.1.1
26
+ Requires-Dist: PyMySQL>=1.2
27
+ Dynamic: license-file
28
+
29
+ # pythia-mysql
30
+
31
+ **English** · [Tiếng Việt](https://github.com/thaildhe172591/pythia-mysql/blob/main/README.vi.md)
32
+
33
+ pythia for MySQL: stored-program development for AI agents, built on
34
+ [pythia-core](https://github.com/thaildhe172591/pythia-core). It aims to do
35
+ what [pythia](https://github.com/thaildhe172591/pythia) does on Oracle and
36
+ its siblings do on PostgreSQL, SQL Server and MongoDB: the agent learns
37
+ from the live catalog, asks a person before any write, and writes only
38
+ through a gate that snapshots, verifies and reports honestly.
39
+
40
+ **Alpha (0.1.0).** It reads everything pythia's Learn movement needs,
41
+ writes through one gate — data rehearsed in a transaction, DDL predicted,
42
+ written once and read back — and wires a project for an agent: the guard,
43
+ the session brief, the approval doors and the `pythia-mysql-*` skills.
44
+
45
+ | Works today |
46
+ |---|
47
+ | reads: `check`, `ls`, `src`, `cols`, `ddl`, `grep`, `deps`, `impact`, `errors`, `connections` |
48
+ | writes: `apply`, `deploy`, `approve`, `journal`, `history`, `policy`, `autopilot`, `lint` |
49
+ | the harness: `install`, `doctor`, `hook`, `mcp`, `skill lint` |
50
+
51
+ MySQL 8.4 LTS is the floor: CI runs the whole live check on 8.4, 9.4 and
52
+ 9.5, and below 8.4 — or on MariaDB — `check` says so (nothing is refused).
53
+
54
+ ## Install
55
+
56
+ ```bash
57
+ pip install pythia-mysql
58
+ ```
59
+
60
+ That brings pythia-core, and [PyMySQL](https://pypi.org/project/PyMySQL/),
61
+ pure Python: no client library needs installing, on Windows, macOS or
62
+ Linux. The development version is the main branch of both:
63
+
64
+ ```bash
65
+ pip install "git+https://github.com/thaildhe172591/pythia-core" \
66
+ "git+https://github.com/thaildhe172591/pythia-mysql"
67
+ ```
68
+
69
+ ## Set up a project for an agent
70
+
71
+ ```bash
72
+ cd your-project
73
+ pythia-mysql install # once; a second run changes nothing
74
+ pythia-mysql doctor # is everything wired? exit 1 names what is not
75
+ ```
76
+
77
+ `install` writes, and leaves everything else as it was:
78
+
79
+ - `.pythia/mysql/connections.json`, mode 600, created once from the
80
+ template — the developer fills in the passwords; an agent never opens it;
81
+ - `.gitignore` lines for that file, this machine's approvals
82
+ (`.pythia/mysql/grants/`) and `.claude/settings.local.json`;
83
+ - `.claude/settings.local.json`: the hooks (the guard, the session brief,
84
+ the AskUserQuestion answer), spelled with this interpreter's absolute path
85
+ so they start whether or not a virtual environment is active, and deny
86
+ rules for the engine's files;
87
+ - the MCP approver `pythia_mysql` (tool `pythia_mysql_approve`), in this
88
+ machine's files only: Claude Code's local scope for the project
89
+ (`~/.claude.json`) and Codex's `~/.codex/config.toml`, each when that
90
+ client has run here;
91
+ - a `pythia-mysql` block in CLAUDE.md and AGENTS.md (other tools' blocks,
92
+ and the developer's own text, are left alone);
93
+ - the skills, in `.claude/skills` and `.agents/skills`:
94
+ `using-pythia-mysql` (the router), `pythia-mysql-explore`,
95
+ `pythia-mysql-impact`, `pythia-mysql-apply`, `pythia-mysql-deploy`,
96
+ `pythia-mysql-autopilot`, `pythia-mysql-verify`.
97
+
98
+ Restart the agent's session after installing, so the hooks load. Each
99
+ session then starts with the rules and a `STATE` line (connections, the
100
+ write policy). Before every tool call the guard decides:
101
+
102
+ | The agent runs | The guard |
103
+ |---|---|
104
+ | `mysql`, `mysqlsh` or `mariadb` given write SQL (DDL, DML, `CALL`, `DO`, `PREPARE`/`EXECUTE`, `LOAD DATA`, `SET GLOBAL`, `SELECT ... INTO OUTFILE`, `LOCK TABLES`, `source file.sql`), a script (`< file`, a pipe into the client, `mysqlsh -f`), MySQL Shell's `util.load*`; `mysqlimport`; `mysqladmin` create, drop, flush, kill, password or shutdown — also behind `docker exec`, `ssh`, `sudo` or `bash -c` | denies: writes go through `pythia-mysql apply` |
105
+ | an MCP SQL tool given write SQL | denies |
106
+ | reading `connections.json`; editing `policy.json`, the journal or a grant; setting `PYTHIA_MYSQL_CI`; calling `grants.mint` from a one-liner | denies |
107
+ | a `pythia-mysql` read, a preview, `approve --card` | allows without a prompt |
108
+ | `apply --confirm <token>` the developer already approved on this connection | allows; apply re-verifies it |
109
+ | `mysql` reads, `mysqldump`, `approve`, `--yes`, `policy set`, anything compound | stays out: the client asks |
110
+
111
+ The guard reads a command for what it names, not what it means: it can
112
+ deny a harmless command (a `SELECT` whose text mentions `drop`) and miss a
113
+ write spelled in a way it does not list. It sees the commands Claude Code
114
+ hands its tools, not Workbench or a driver inside the application. The gate
115
+ behind it holds either way (pythia-core's SECURITY.md).
116
+
117
+ pythia-postgres, pythia-mssql and pythia-mongo install beside it in one
118
+ project, and pythia for Oracle too: each keeps the others' hooks and
119
+ instruction blocks, and each guard and approval hook acts for its own tool
120
+ only (pythia-core's CI checks it with all five installed).
121
+
122
+ ## Connections
123
+
124
+ `.pythia/mysql/connections.json` in the project root, owner-readable only
125
+ (`chmod 600`), and out of git. Template:
126
+ [examples/connections.example.json](https://github.com/thaildhe172591/pythia-mysql/blob/main/examples/connections.example.json).
127
+
128
+ ```json
129
+ {
130
+ "default": "dev",
131
+ "dev": { "host": "db-dev", "port": 3306, "database": "app_owner",
132
+ "user": "app_agent", "password": "...", "tls": "request" }
133
+ }
134
+ ```
135
+
136
+ - `host`, `database`, `user` are required. In MySQL a schema is a
137
+ database: `database` is where an unqualified name lands, and what pythia
138
+ reads unless `schema` names another (then `check` warns).
139
+ - `tls` is `off`, `request` (the default: TLS whenever the server offers
140
+ it), `require` (encrypted or refused, the certificate not checked) or
141
+ `verify` (the certificate and the host name checked, against `tls_ca`
142
+ when it is set, else this machine's CAs).
143
+ - An account on `caching_sha2_password` — MySQL's default — needs TLS, or
144
+ the `cryptography` package, to send its password; when neither is there,
145
+ pythia-mysql says so instead of failing obscurely.
146
+ - The entry is picked by `--conn NAME`, then `PYTHIA_MYSQL_CONNECTION`, then
147
+ the only entry, the folder directly under the project root, then
148
+ `"default"`. `PYTHIA_MYSQL_CONFIG` points at another file.
149
+
150
+ pythia for Oracle keeps `.pythia/connections.json`, and the other engines
151
+ `.pythia/postgres/`, `.pythia/mssql/` and `.pythia/mongo/`; pythia-mysql
152
+ reads none of them.
153
+
154
+ ## Commands
155
+
156
+ ```bash
157
+ pythia-mysql check # server, database, where unqualified names land
158
+ pythia-mysql ls "t_order%" # tables, views, routines, triggers, events by LIKE
159
+ pythia-mysql src f_order_total # a routine, view, trigger or event, rebuilt
160
+ pythia-mysql cols t_order # columns, types, defaults, generated columns
161
+ pythia-mysql ddl t_order # CREATE TABLE and its triggers, rebuilt
162
+ pythia-mysql grep "t_order where" # every line of code holding a text
163
+ pythia-mysql deps p_order_paid # what it uses
164
+ pythia-mysql impact t_order # what uses it: run before changing it
165
+ pythia-mysql errors [NAME] # views MySQL has marked broken; what it cannot check
166
+ ```
167
+
168
+ `--json` gives one envelope per command; `--limit`, `--max-lines` and
169
+ `--offset` window long output, and any truncation says how to fetch the
170
+ rest.
171
+
172
+ ### What the catalog cannot see, and how pythia-mysql says so
173
+
174
+ - **`impact` asks the catalog, then searches bodies.** information_schema
175
+ records which tables, views and functions a view reads, the triggers on a
176
+ table and the foreign keys that point at it — and nothing about what a
177
+ procedure, function, trigger or event body uses. Those bodies, in every
178
+ schema, are searched for the name as a whole word (a qualifier names the
179
+ schema, none means the body's own), and those rows are marked `[text]`.
180
+ Each blind spot is printed every time.
181
+ - **MySQL hides what an account may not see.** A routine needs
182
+ `SHOW_ROUTINE` (or a global `SELECT`) to be seen with its body, a trigger
183
+ the `TRIGGER` privilege, an event `EVENT`. pythia-mysql reads `SHOW
184
+ GRANTS` and says which of them this account cannot see, every time it
185
+ could matter.
186
+ - **`errors` reads what MySQL has marked broken.** Dropping or altering
187
+ what a view reads marks the view invalid, and pythia-mysql lists the
188
+ error it would raise. MySQL resolves the names inside a procedure,
189
+ function, trigger or event only when it runs: `errors` names them as not
190
+ checked, and never calls that clean.
191
+ - **`src` and `ddl` are rebuilt, not dumped.** MySQL keeps no CREATE
192
+ statement in information_schema and no line numbers: the header is
193
+ rebuilt on line 1, and the body follows as it was written (a view's
194
+ SELECT only as MySQL rewrote it). `ddl` wraps code in `DELIMITER //`, as
195
+ the mysql client and Flyway read it, and notes the `sql_mode` it was
196
+ created under. The live check runs every `ddl` into a copy and compares
197
+ it with the original.
198
+ - **Reads cannot write.** Every read command's session is `READ ONLY`, and
199
+ MySQL refuses DML and DDL alike there (error 1792). A read-only account is
200
+ the first line.
201
+
202
+ ### Writing: the gate
203
+
204
+ ```bash
205
+ pythia-mysql apply change.sql # preview: nothing written
206
+ pythia-mysql approve 3fa9c1 # the developer approves (or from chat, below)
207
+ pythia-mysql apply change.sql --confirm 3fa9c1 # writes it — only as approved
208
+ pythia-mysql journal restore <entry> # undo is a write too: preview, approve, confirm
209
+ pythia-mysql policy # which groups may be written, and is rollback real?
210
+ ```
211
+
212
+ The developer approves through the first door that works:
213
+
214
+ 1. **In Claude Code's chat.** The agent asks with AskUserQuestion, its text
215
+ exactly the output of `pythia-mysql approve --card <token>`, options
216
+ Approve / Reject. The answer hook records the approval only when the
217
+ question carried that card and the developer chose Approve themselves —
218
+ not an answer the agent wrote, not a dialog that closed unattended.
219
+ 2. **Through the MCP approver** (`pythia_mysql_approve`; Codex starts
220
+ here): the client shows the card, and only an explicit Approve counts.
221
+ 3. **At the developer's console:** `pythia-mysql approve <token>`.
222
+
223
+ `pythia-mysql doctor` warns about settings that would answer the chat door
224
+ for the developer (`dontAsk` mode, an allow rule for AskUserQuestion, a hook
225
+ matching every tool).
226
+
227
+ ### A release: `deploy`, one transaction
228
+
229
+ ```bash
230
+ pythia-mysql deploy 01_fix.sql 02_backfill.sql # preview the release as one transaction
231
+ pythia-mysql deploy 01_fix.sql 02_backfill.sql --confirm 9c41e0b2aa # all of it, as approved, or none
232
+ ```
233
+
234
+ A release is data: one or more scripts of `INSERT`, `REPLACE`, `UPDATE` and
235
+ `DELETE`, cut at each `;` (and at `DELIMITER` lines, as the mysql client
236
+ cuts them). It is rehearsed in one transaction: each statement in order,
237
+ its rows taken at its own point, then everything rolls back. One card shows
238
+ every statement, and one approval (a 10-digit token, through the same
239
+ doors) covers the release; a single statement's token cannot be approved
240
+ alone. The confirm checks that every statement is byte-identical to what
241
+ was approved before anything runs, re-runs the release in one transaction,
242
+ and commits only if every statement still matches. A statement that fails,
243
+ or rows that moved, roll back the whole release.
244
+
245
+ MySQL commits DDL as it runs it, so a release cannot hold it: a table, a
246
+ column, an index, a view, a routine, a trigger, an event, a comment or a
247
+ grant is refused, with nothing written — apply each on its own, before or
248
+ after the data. A `SET` inside a release is refused too, and anything
249
+ `apply` refuses. Rows the release itself inserts get a new `AUTO_INCREMENT`
250
+ value, `CURRENT_TIMESTAMP` or `UUID()` every time it runs, so an UPDATE or
251
+ DELETE of them later in the same release never matches its approval: give
252
+ them their final values in the INSERT.
253
+
254
+ ### Lint: before any apply
255
+
256
+ ```bash
257
+ pythia-mysql lint 01_table.sql 02_view.sql # a change's files in one run
258
+ pythia-mysql lint --rules # every rule and its effective severity
259
+ ```
260
+
261
+ The rules read what MySQL accepts and a reviewer would not:
262
+
263
+ | Rule | Default | Why |
264
+ |---|---|---|
265
+ | `mysql-non-transactional` | error | a MyISAM, MEMORY or other table without transactions cannot be rehearsed, can lose rows in a crash, and ignores foreign keys |
266
+ | `mysql-utf8mb3` | warn | `utf8` is `utf8mb3`: no emoji and no rarer CJK — `utf8mb4` |
267
+ | `mysql-select-star` | warn | MySQL expands `*` when a view is created and keeps that list; a procedure's result changes shape; `EXISTS (SELECT * ...)` is fine |
268
+ | `mysql-not-in-subquery` | warn | `NOT IN (SELECT ...)` is never true when the subquery returns a NULL |
269
+ | `mysql-primary-key` | warn | without one, InnoDB invents a hidden key that replication and row-based tools cannot use |
270
+ | `mysql-table-comment` | warn | a new table gets its `COMMENT` in the same change (an `ALTER TABLE ... COMMENT` in the same run counts) |
271
+ | `mysql-function-characteristic` | warn | a function declaring none of `DETERMINISTIC`, `NO SQL`, `READS SQL DATA` is refused while binary logging is on (1418) |
272
+ | `mysql-definer` | warn | a `DEFINER` naming an account needs `SET_ANY_DEFINER`, and breaks where that account does not exist |
273
+ | `mysql-zero-date` | warn | `'0000-00-00'` is refused under MySQL's default `NO_ZERO_DATE` |
274
+ | `mysql-limit-order` | info | `LIMIT` without `ORDER BY` in stored code takes whichever rows come first |
275
+
276
+ What MySQL rejects itself is left to the rehearsal or the prediction. The
277
+ team sets severities and naming patterns in `.pythia/mysql/lint.json`:
278
+
279
+ ```json
280
+ {"severity": {"mysql-select-star": "error", "naming": "error"},
281
+ "naming": {"PROCEDURE": "^p_[a-z0-9_]+$", "VIEW": "^v_[a-z0-9_]+$"}}
282
+ ```
283
+
284
+ Every preview shows the findings, a statement's errors are on its card, and
285
+ autopilot never lands a lint error.
286
+
287
+ ### Autopilot: one lease instead of every approval
288
+
289
+ ```bash
290
+ pythia-mysql autopilot request --groups source,comments --hours 2 --max-writes 30
291
+ pythia-mysql autopilot report # every write under the lease, with its restore
292
+ pythia-mysql autopilot off # anyone may revoke
293
+ ```
294
+
295
+ When the developer wants the agent to work without approving each write,
296
+ the agent requests a lease. Its card names the connection, the groups, the
297
+ time, the number of writes and whether destructive statements are included,
298
+ and nothing is delegated until the developer approves it through a door (or
299
+ runs `pythia-mysql autopilot on` at a console). While it lasts, `apply` and
300
+ `deploy` land what it covers without `--confirm` — still snapshotted,
301
+ rehearsed or predicted, read back and journaled. It never covers another
302
+ connection, one marked `"protected": true` (or an `"env"` of prod /
303
+ production / prd / live), a group or destructive statement the card did not
304
+ name, or more than 1000 rows; a write that breaks something, lands unlike
305
+ its prediction or is `WRITTEN?`, or five failures in a row pause it.
306
+
307
+ ### One statement: `apply`
308
+
309
+ One statement per file. A stored program (`CREATE PROCEDURE`, `FUNCTION`,
310
+ `TRIGGER`, `EVENT`) takes its whole file, its body's `;` included, and
311
+ nothing may follow its body; `DELIMITER` lines are read as the mysql client
312
+ reads them, so `ddl`'s output applies as it is. Anything else ends at its
313
+ `;`. `apply` sorts the statement into a group, and the policy decides:
314
+ `source` (views, procedures, functions, triggers, events) and `comments`
315
+ (`ALTER TABLE ... COMMENT = '...'`, alone) need the developer's approval;
316
+ `data_dml`, `structural` (DDL) and `grants` (`GRANT` and `REVOKE` on a
317
+ schema or an object in it) are denied until the developer runs
318
+ `pythia-mysql policy set <group> confirm` at a console. `session` (`SET`)
319
+ is allowed.
320
+
321
+ MySQL commits DDL as it runs it, so the write path is split in two:
322
+
323
+ - **Data is rehearsed.** An INSERT, REPLACE, UPDATE or DELETE runs inside
324
+ the transaction and is rolled back; the confirm runs it again and commits
325
+ only if it still matches. An UPDATE or DELETE is approved on its rows:
326
+ the count, a sample and a fingerprint over every column (an order-free
327
+ sum of `md5(json_array(...))`); if any row joins, leaves or changes before
328
+ the confirm, the confirm is refused. An UPDATE counts the rows it
329
+ matched, as the check does, not only those whose value changed.
330
+ - **A rehearsal cannot write where MySQL cannot roll back.** A statement
331
+ whose own table is MyISAM, MEMORY or another engine without transactions
332
+ is refused before it runs. A trigger or function that writes to such a
333
+ table would turn a preview into a write, so every data change runs first
334
+ inside a savepoint: when rolling that back draws MySQL's warning 1196,
335
+ the preview is reported **`WRITTEN?`** with exit 3 — those rows are
336
+ written — and never as nothing written. The statement therefore runs
337
+ twice in a rehearsal; the preview says so. A server that enforces GTID
338
+ consistency refuses such a statement itself (error 1785), before
339
+ anything is written.
340
+ - **DDL is predicted — nothing runs before the approval.** A view's SELECT
341
+ runs with `LIMIT 0`, so its names must resolve. The views MySQL would
342
+ mark invalid are read from information_schema: every view that reads a
343
+ table, view, function or column the statement drops or renames, and
344
+ every view on top of those, in any schema. A routine, trigger or event is
345
+ `NOT VERIFIED` — MySQL checks its names only when it runs — and the
346
+ routines that name what a statement drops are listed as `[text]` leads.
347
+ After the approval the statement runs once; the views MySQL marked
348
+ invalid are read back, and anything unlike the prediction exits 3, as a
349
+ breakage does. A prediction that moved since the approval refuses the
350
+ write.
351
+ - **A CREATE for a routine that exists replaces it.** MySQL has no `CREATE
352
+ OR REPLACE` for a procedure, function, trigger or event. A procedure or
353
+ function is first created under a scratch name, so that MySQL's verdict
354
+ on the new text costs nothing; then the old one is dropped and the new
355
+ one created, and if MySQL refuses it the old one is put back from the
356
+ snapshot (if that fails too, it is **`WRITTEN?`**, exit 3, with the
357
+ restore). A trigger or an event is dropped and created; rows written in
358
+ between fire neither trigger. A view is replaced with `CREATE OR REPLACE
359
+ VIEW`, which MySQL does in one step.
360
+ - **The definer is said.** A replacement without a `DEFINER` clause is
361
+ defined by this account; the preview says so when that changes it.
362
+ Putting back another account's routine or view needs `SET_ANY_DEFINER`
363
+ (and `SYSTEM_USER` when that account is a system one): when this account
364
+ lacks it, the preview says the old one could not be put back by it, and a
365
+ restore it tries is refused with nothing written.
366
+ - **Refused, with the reason:** `CALL`, `DO`, `PREPARE` and `EXECUTE`;
367
+ compound statements outside a stored program; `SELECT`, `SHOW`, `WITH`;
368
+ `USE`; transaction control and `LOCK TABLES`; `LOAD DATA`; temporary
369
+ tables; accounts, roles, databases and other server-level statements
370
+ (`SET GLOBAL`, `FLUSH`, `GRANT ... ON *.*`); table maintenance
371
+ (`OPTIMIZE`, `ANALYZE`); `IF NOT EXISTS` on a stored program;
372
+ executable comments (`/*! ... */`); several statements in one file; an
373
+ UPDATE or DELETE whose rows cannot be read beforehand (`IGNORE`, `ORDER
374
+ BY ... LIMIT`, several tables, a join, a subquery, a `PARTITION` clause,
375
+ parentheses that do not balance); and a routine whose body this account
376
+ cannot read (nothing to snapshot).
377
+
378
+ pythia-mysql never turns on `MULTI_STATEMENTS` or `LOCAL INFILE`: MySQL
379
+ itself refuses any text after the one statement sent. A write waits at most
380
+ 5 s for a lock (`innodb_lock_wait_timeout`, `lock_wait_timeout`). A
381
+ rehearsal still fires triggers and draws `AUTO_INCREMENT` values, and the
382
+ preview of a data change says so. Exit codes follow pythia: `0` clean, `1`
383
+ refused, `3` written but broken, or written unlike the prediction (with the
384
+ restore command).
385
+
386
+ ### Least privilege
387
+
388
+ The read commands need `SELECT` and `SHOW VIEW` on the schemas, and
389
+ `SHOW_ROUTINE` to read routine bodies:
390
+
391
+ ```sql
392
+ CREATE USER 'app_agent'@'%' IDENTIFIED BY '...';
393
+ GRANT SELECT, SHOW VIEW ON app_owner.* TO 'app_agent'@'%';
394
+ GRANT SHOW_ROUTINE ON *.* TO 'app_agent'@'%';
395
+ ```
396
+
397
+ MySQL shows triggers only to an account holding `TRIGGER`, and events only
398
+ with `EVENT` — both also let it change them. Without them, pythia-mysql
399
+ says that triggers and events are hidden; the live check runs every read
400
+ command both ways.
401
+
402
+ A write needs what the change needs, on its schema only — for example
403
+ `INSERT, UPDATE, DELETE` for data, `CREATE, ALTER, DROP, INDEX, CREATE
404
+ VIEW, CREATE ROUTINE, ALTER ROUTINE, TRIGGER, EVENT` for DDL. An account
405
+ holding every privilege on the server, `SUPER`, `SYSTEM_USER`,
406
+ `SET_ANY_DEFINER` or `SET_USER_ID` is named in the preview: far more than a
407
+ change needs. The live check runs every write path as an account whose
408
+ write privileges stop at its schema.
409
+
410
+ ## Tests
411
+
412
+ ```bash
413
+ python tests/test_mysql.py # no database, no PyMySQL
414
+ python tests/live_check.py # a disposable server only (see its docstring)
415
+ ```
416
+
417
+ CI runs the suite on Ubuntu, Windows and macOS, Python 3.9 and 3.13, and
418
+ the live check — every read, every write path, a release, a lint finding in
419
+ a real preview and a write under a lease — against MySQL 8.4, 9.4 and 9.5
420
+ in service containers. It tests against the pythia-core branch of the same
421
+ name when one exists.
422
+
423
+ ## License
424
+
425
+ MIT