Skip to content

Paging Trino, and the session settings that do not stick

The clause order is transposed for you and a trailing semicolon is a syntax error, while a session setting succeeds and then affects nothing.

The shared query limiter in LibreDB Studio appends a LIMIT ... OFFSET ... clause to fetch the second page of a result. Measured against Apache Trino 476, that statement answers line 1:47: mismatched input 'OFFSET' and stops. Nothing about the query is wrong; the clause order is. Two more editor-level facts sit next to that one, and the third does not fail at all.

The clause order this grammar wants

Trino’s grammar is [ OFFSET count ] [ LIMIT count ], and only that way round. Measured against trinodb/trino:476:

SELECT nationkey FROM tpch.sf1.nation LIMIT 3 OFFSET 1
-- line 1:47: mismatched input 'OFFSET'. Expecting: <EOF>

SELECT nationkey FROM tpch.sf1.nation OFFSET 1 LIMIT 3
-- rows

That matters more than a typo would, because a results grid does not ask a person to write the pagination clause; it appends one. Every page after the first is a statement the tool generated, so a generator emitting LIMIT before OFFSET fails every paged read, while the first page, which carries no offset, succeeds.

Why the limiter transposes rather than passes through

The shared query limiter in LibreDB Studio is one piece of code for every SQL engine. It decides where the clause goes, which is the hard part: it inserts before a trailing comment, and it refuses a statement whose end it cannot safely cut, because the reader that finds the end scans spans rather than matching a regular expression against the last few characters. A statement whose string literal or block comment never closes is left alone instead of being truncated at the wrong place.

That work is correct on Trino. Only the clause order is wrong. So the Trino provider overrides one inherited helper, prepareQuery(): it calls the shared implementation first and then transposes the two clauses. It does not rewrite the statement, and it does not carry a second copy of the limiter’s placement logic.

Which occurrence gets transposed is decided by reconstruction rather than by position, and that is not defensive coding. Because the clause is inserted before a trailing comment, lastIndexOf finds the text inside a comment on a statement that quotes its own bound, and indexOf finds a subquery’s. Exactly one occurrence is the appended one: the one whose removal, together with the single space in front of it, yields the original statement back. That is the test the code applies.

Paging is therefore real on Trino, and the transposition never reaches the editor.

The trailing semicolon that is a syntax error

SELECT 1; is a syntax error on Trino: line 1:9: mismatched input ';'. The statement endpoint takes one statement and no terminator, which the provider declares as statementTerminator: "none". That declaration is load-bearing rather than cosmetic, because the shared statement generators read it; without it they emit statements this engine refuses.

The transport then drops a single trailing semicolon before the statement leaves it, so query("SELECT 1;") and query("SELECT 1") reach the coordinator as identical bytes. Trailing whitespace and a newline after the semicolon count as trailing too, which is what a statement pasted out of a file carries.

It is a strip, not a splitter, and the difference is the whole of it:

You send Reaches the wire as Why
SELECT 1; SELECT 1 The terminator, dropped
SELECT 1 ;\n SELECT 1 Whitespace after it is trailing too
SELECT 1; SELECT 2 unchanged The endpoint takes exactly one statement
SELECT 1;; unchanged A doubled terminator is a second, empty statement
SELECT ';'; SELECT ';' A semicolon inside a literal is data
SELECT 1 -- done; unchanged A semicolon inside a comment is prose
SELECT 1; -- done unchanged Declared limit: a terminator with a comment after it is left alone

The last row is a boundary, not an oversight. A rule with an undocumented edge is worse than a narrower rule with a stated one.

Statements that succeed and affect nothing

The first two facts announce themselves with an error. This one does not.

There is no connection to Trino in the usual sense. The provider speaks Trino’s own client protocol over HTTP - POST /v1/statement, then a chain of nextUri links - with no driver of any kind and no connection pool. Each statement is sent on its own connection. None of the session the coordinator offers back is kept.

So SET SESSION, USE, PREPARE and DEALLOCATE all report success, and then have no effect on the next statement. Nothing in the answer distinguishes them from a statement that worked, because as far as the engine is concerned one did. Set a session property, run your query, and the query ignores the property. Type USE tpch.tiny, then run a statement with a bare table name, and the name does not resolve. The engine reported success both times.

That is the boundary this engine carries, and it is stated rather than smoothed over: session settings, catalog selection and prepared statements report success and have no effect on the next statement. Each of those four operations therefore attaches a warning to its own result:

"SET SESSION" succeeded, but each statement is sent on its own connection, so it will not affect the next one. Qualify names in full instead.

A warning is a strange thing to attach to a success. It is the right thing here, because the alternative is a user who watches four statements succeed and then debugs the fifth. The engine’s own remarks travel the same channel - a redundant ORDER BY in a subquery answers with rows plus REDUNDANT_ORDER_BY - de-duplicated, because the same remark repeats on every page of the exchange and a caller must not render it six times.

The same statelessness is why transactions are not offered on Trino at all. Trino has START TRANSACTION, but a transaction lives in an HTTP session header this provider does not carry between statements, so the controls are absent rather than present and broken.

Qualifying names in full instead

The warning ends with the remedy, and the remedy is the way the rest of the integration already works. Every name the tool generates is fully qualified, schema.table, because Trino resolves a bare name only when the session has a schema - and there is no session.

The connection’s Database field pins one catalog, sent as X-Trino-Catalog on every request. That field is a catalog, not a database, and what it decides is which catalog the schema tree shows: two levels, catalog to schema to table, every table displayed schema.table. It does not constrain the editor. A cross-catalog join runs exactly as typed as long as the names are fully qualified, and a connection that pins no catalog still connects and still runs every fully qualified statement - it just has no tree, and the schema read says that is why rather than showing an empty one.

So the working habit on Trino is the habit the warning asks for. Write catalog.schema.table in the editor and the missing session cannot change what a name resolves to. Write a USE and it can. The engine’s published line on the engine pages names the neighbouring boundary: Trino queries catalogs, it does not manage a lakehouse, and writes depend on the underlying connector. Measured, a connector that accepts CREATE TABLE answers UPDATE with This connector does not support modifying table rows, and that refusal is shown verbatim.

Two of these three facts the tool absorbs and you never see. The third arrives as a warning on a statement that succeeded, because that is the only place a reader can still act on it. What the editor generates per engine, and what it does not, is written down on the features page.