An AI with SQL and good manners
"Give a language model a database connection and it will find the one query you did not think of. Before lunch."

Letting an assistant write SQL over your spreadsheet is the whole point of tableski. It is also how you end up with a model that helpfully tidies your data by deleting it. The assistant is not malicious; it is eager, and eager is enough. So the hosted service treats every query as coming from someone it does not trust, and decides what the statement may do before a single row is read.
Decisions on the statement, not on the text
The naive way is to scan the SQL for scary words. It fails in both directions: SELECT * FROM dropbox_exports contains "drop", and a comment or a clever alias hides the real intent from any pattern. tableski parses the statement with the same parser the engine uses and looks at the tree:
- Exactly one statement. Two statements separated by a semicolon are refused as a pair, whatever the second one is.
- The statement must be a query: SELECT, WITH, VALUES, or EXPLAIN of one of those. CREATE, INSERT, UPDATE, DELETE, DROP, COPY, SET and their relatives are refused by kind, with a message that names the kind.
- Every table it touches must be one of yours, or a name the query itself defines. File paths, URLs, catalog tables and other people's tables are unknown tables.
- Table functions that read the filesystem or generate data are refused anywhere in the query, including inside a subquery or a WITH clause.
Then the engine itself gets the same instruction a second time, through its own switches that disable data definition and data manipulation. Two fences, one message to the client.
Read-only is not the same as harmless
A query can be perfectly read-only and still take the machine down: a cross join of two tables, a result with a hundred million rows, an expression that builds a gigabyte string. So every query is also bounded. There is a memory pool it cannot grow past, a wall-clock limit after which it is cancelled, and a cap on the rows and bytes it may hand back. A result that hits the cap comes back cut, with a line saying so and suggesting a LIMIT or an aggregate. An export that would be cut is refused instead, because a partial file that looks whole is worse than no file.
One shape needed special treatment. A cartesian product, a cross join or a join without an equality, yields work so quickly per unit of progress that a timeout cannot interrupt it in time. Those are refused before they start, on the plan the engine produces, with a note to join on a column with an equals sign.
The hostile suite
All of this is a test file that runs before every change: about fifty statements, from CREATE EXTERNAL TABLE pointing at /etc/passwd through homoglyph table names, null bytes, two thousand nested parentheses, a megabyte of SQL, an infinite recursive CTE and a two-hundred-way union, plus a set of file names for the export tool that try to leave their folder. Each must be refused or bounded within ten seconds, and the server must answer the next question afterwards. When we found that the optimizer would happily spend nineteen seconds folding repeat('x', 200000000) at planning time, that became a case too, and planning moved inside the same time limit as execution.
What the assistant sees
Plain errors that say what to do: rejected: DROP TABLE is not allowed for untrusted clients; only read-only queries over registered tables run here. Models read that and correct themselves; they are good at following instructions that arrive as answers. The open-source binary you run on your own laptop trusts you by default, because it is your data and your machine; the same guard is one flag away, --untrusted-sql, when you serve others.