Skip to content
mcp-data-platform composable mcp data platform
v1.x part of txn2 ↗

Registered Tables

A file that reaches the platform - a CSV a person uploaded as a managed resource, or one trino_export wrote as a portal asset - can be registered as a table on a Trino connection and joined to warehouse tables from then on.

Nothing is copied. The registration creates an external table over the directory the file already sits in, so the table reads whatever the object holds now: a vendor drop that overwrites its file changes the query result on the next run with no further action. A new version of the file - a revision of a managed resource, an edit of an asset, a script's refresh - moves the table onto it as well, because a registration follows its file unless it was pinned.

What it is for

Getting a short list of outside keys into a report is otherwise awkward. Trino has no client-side upload, and an agent emitting INSERT ... VALUES batches for a fifty-thousand-row vendor CSV is slow and burns context. The join is the easy part; loading is the gap.

A few hundred keys need no table at all - they join inline with JOIN (VALUES ('a'),('b')) AS t(id) through trino_query on a read-only connection. Registration is for the file that is too big for that.

What an operator has to configure

Registration is available on a Trino connection that names a scratch target: the catalog and schema registrations are written into.

toolkits:
  trino:
    enabled: true
    instances:
      warehouse:
        host: trino.example.com
        user: "${TRINO_READONLY_USER}"
        catalog: warehouse
        read_only: true
      scratch:
        host: trino.example.com
        # A DISTINCT Trino identity, whose access-control rules allow DDL only
        # on the scratch catalog. See "What the scratch schema is" below.
        user: "${TRINO_SCRATCH_USER}"
        password: "${TRINO_SCRATCH_PASSWORD}"
        catalog: scratch
        schema: uploads
        read_only: false
        scratch:
          catalog: scratch
          schema: uploads

Both connections above reach the same coordinator over HTTPS. On a coordinator that speaks plain HTTP, each connection has to say so with ssl: false and its port: a connection that never mentions ssl is assumed to be HTTPS on 443 unless it is the one named by default: or its host is localhost. See ssl.

A connection with no scratch: block cannot hold a registration, and the surfaces do not offer one on it. Both keys are required: a block naming only one is ignored with a warning, because a registration built on it would fail at the DDL.

read_only: false on the scratch connection is not decoration either. A scratch target says where a registration writes; it grants nothing. The statement that creates the table is write SQL, so a read_only: true connection refuses it however its target is configured — and such a connection is not offered, for the same reason one with no target is not. Naming it directly anyway — through manage_table, or a form built before an administrator flipped the flag — is refused with 400 and the sentence "this connection is read-only, so a table cannot be created on it; ask an administrator for a connection that accepts writes", the same class of answer as a connection with no scratch target. It is not a 500: the connection is working exactly as configured, and reporting a configuration fact as a platform outage told the person neither which connection nor why. What keeps a registration off the warehouse is not this flag but the Trino identity the connection authenticates as.

The catalog itself is a Hive connector over the same object store the platform's managed resources and portal assets live in. A file metastore is enough:

# etc/catalog/scratch.properties
connector.name=hive
hive.metastore=file
hive.metastore.catalog.dir=s3://<bucket>/trino-metastore/
hive.recursive-directories=false
fs.native-s3.enabled=true
s3.endpoint=<your object store endpoint>
s3.path-style-access=true
s3.region=us-east-1
s3.aws-access-key=${ENV:SCRATCH_S3_KEY}
s3.aws-secret-key=${ENV:SCRATCH_S3_SECRET}

Trino needs its own credentials to the bucket. They are separate from the platform's S3 connection credentials and are configured on the Trino cluster, not here.

Registering

In the portal. Open a CSV resource or a CSV asset. A Query as a table section offers the connections you can reach that can hold one, and shows what is already registered with the columns each table has.

From a session. manage_table carries three actions, on either kind of stored file:

manage_table action=register   reference=mcp:resource:... connection=scratch
manage_table action=register   reference=mcp:resource:... connection=scratch follow=false
manage_table action=register   reference=mcp:resource:... connection=scratch repair=true
manage_table action=list       reference=mcp:asset:...
manage_table action=unregister registration_id=...

reference is the string a search hit or a fetch document carries, passed verbatim: mcp:resource:<id> for material somebody uploaded, mcp:asset:<id> for a saved asset. The kind travels inside the reference, so one action serves both and there is no argument naming which is which. table_name is optional; the default is a slug of the file's name. follow defaults to true: the table is moved onto each new revision or version written over the file. follow=false is what the second call above adds: it pins the table to the version of the file it is registered over, for a report that must keep returning the same rows until somebody registers it again. See Following the file. repair is what the third call adds: it saves a corrected version of a file that cannot be read as a table the way it is stored, and registers that. See A CSV a query engine cannot read.

A reference that names no file you may register - one that does not exist, one that was deleted, one belonging to somebody else - is answered the same way in every case, so the tool cannot be used to find out which files exist.

Over REST.

GET    /api/v1/table-connections
GET    /api/v1/resources/{id}/tables
POST   /api/v1/resources/{id}/tables       {"connection": "scratch", "table_name": "vendor_keys", "follow": true, "repair": false}
DELETE /api/v1/resources/{id}/tables/{registrationID}
GET    /api/v1/portal/assets/{id}/tables
POST   /api/v1/portal/assets/{id}/tables
DELETE /api/v1/portal/assets/{id}/tables/{registrationID}

Finding what is registered

The scratch schema is shared: everyone granted the connection sees every table in it. So the question "what is registered here" is one a reader has to be able to ask of the platform rather than of one file at a time.

Scratch Tables, in the portal's own section list, answers it. One list of every registration, whichever kind of file each was built over: the qualified name to write in a FROM clause, the connection it lives on, the file behind it, how many columns it has, who registered it and when, and whether it is still reading that file's current contents.

Scratch TablesScratch Tables

What you see is decided by the connection, which is the boundary Trino itself applies: the registrations on connections your persona is granted, and an administrator sees all of them. It is deliberately wider than the register form's connection list, which narrows further to connections that can hold a new table. A connection turned read-only after a registration would otherwise drop that table out of the list for the person who made it, while the engine went on answering queries against it.

The state column says which rule each table is under and whether it is keeping to it. Follows the file is the ordinary registration: each new version written over the file moves the table onto it. Pinned is a registration made with follow off, which stays on the version it was registered over until somebody registers it again. Two exceptions are called out because nothing else can report them. Behind the file is a registration whose source has a newer revision or version than the table points at: a pinned one by design, or a following one whose last follow could not move it, in which case the reason is on the registration. Source deleted is a registration whose file is no longer on the platform; deleting a file unregisters its tables, so this is the residue of a cleanup that did not complete.

Opening a row opens the registration at an address of its own: the sample statement with the cast a join needs, the columns with their types, the file it came from, and the directory it reads.

One registered tableOne registered table

Unregistering is offered here on the tables you may drop, which is the rule the per-file panel applies: authority over the file, and having registered the table yourself or being an administrator. Registering stays on the file's own page, because it needs the file - the platform reads the header row to learn the columns.

A table behind its fileA table behind its file

A table that follows its file and could not be moved says why, on the row and on its own page, until it is registered again.

A following table whose last follow failedA following table whose last follow failed

The listing is driven by the stored registrations rather than by what can be registered, so a source of a format the register action does not yet accept appears here as soon as one exists.

GET /api/v1/tables?connection=&kind=&q=&page=&per_page=
GET /api/v1/tables/{registrationID}

kind is resource or asset; q matches the qualified name. A registration on a connection you do not reach answers 404, the same as one that does not exist.

The lifecycle

Registration is a pointer and a name, not an import. Six things can happen to one, and the portal's Query as a table panel is where four of them are done.

Register. Pick a connection and, optionally, a name. The platform reads the file's header for column names, creates the external table over the directory the file occupies, and records the registration.

A registration that fails partway leaves nothing describing something that is not there, in either direction, and the answer tells the caller to register again. A table whose row could not be written is dropped: nothing would list it, and registering the same file again would meet it in Trino and fail on a name already taken. A table still recorded by the registration it was replacing is dropped for the opposite reason -- it now reads the new file with the new columns while the row that survived goes on naming the old ones, and nothing marks that. And a replacement whose CREATE failed after its DROP had already run forgets the row of the registration it took over, because the table that row named is gone. A replacement whose DROP is the statement that failed changed nothing and is left exactly as it was.

Register again. The control stays available after the first registration, because a file is not limited to one. Registering again does one of two things, depending on the name:

  • A different name, or the same name on a different connection, adds a second table over the same file. Both stay listed, both work, and neither knows about the other. This is how one CSV is queryable from two clusters, or under a name that reads better in one team's queries than another's.
  • The same name on the same connection replaces the registration, provided it is a name you registered. This is the repair for staleness below, and it is why the panel's warning says to register again rather than to unregister first. A name somebody else registered is refused and names who holds it, so nothing is silently overwritten; an administrator is unrestricted and does replace it.

Follow the file. A registration made with follow on - the default - is moved onto each new revision or version written over the file, by the write that produced it. See Following the file.

Correct the file. A registration made with repair saves a corrected version of a file that cannot be read as a table the way it is stored, and registers that version. A following one keeps doing it: each later version carrying a defect of the same kind is corrected too. See Correcting the file.

Go stale. A registration made with follow off stays on the version it was registered over, and a following one stays where it was when its last follow could not move it. The table keeps working; it is serving content that is no longer current, and the write that put it there said so.

Unregister. Drops the table and forgets the registration. The file is not touched: dropping a Hive external table removes the catalog entry and nothing else, and the object stays exactly where it was, byte for byte. Unregistering is the registrant's call or an administrator's.

Delete the file. Deleting the resource or asset drops every table registered over it, whoever registered them, and forgets the registrations.

A CSV a query engine cannot read

Trino's Hive CSV reader is line-based: the text input format splits records on newlines before the quote-aware serde sees them. A line break inside a quoted cell therefore tears one record into several, the first fragment ends on an unbalanced quote, and every field after it lands in the wrong column.

This is the shape a spreadsheet export takes whenever one cell holds a multi-line value - an address, a note, a pasted paragraph. Such a file parses perfectly under every ordinary CSV reader, so nothing about it looks wrong, and a table over it is created without error and answers queries with rows. The rows are fragments.

The same reader splits on the newline and on nothing else, so a file whose lines end in a bare carriage return - the classic Mac ending some spreadsheet exports still write - is one record to it: a table over that file has a single row holding the whole file, and it too is created and queried without error.

Registration therefore reads the whole file before it creates anything - it is already reading it for the header row - and refuses three things:

  • Lines that end in a carriage return. The records that end in one are run together into a single row, and the refusal says what the lines end in. A Windows CRLF ending is not this: every reader on the path folds it to a newline, and it costs nothing.
  • A line break inside a cell. The refusal says how many rows carry one and which columns they are in.
  • Bytes that are not UTF-8. They reach every cell as replacement marks: a cell reading 15% in the source arrives as 15% followed by mojibake. A NUL byte is refused on the same ground even where the rest of the file is valid UTF-8, for the reason under Correcting the file below.

None of the three is refused silently and none leaves anything behind: no table is created and no registration is recorded.

The line endings are settled before the rest of the file is read, so a refusal counts the records the file holds and names the columns it declares, rather than the single record a reader that splits on the newline alone found in it. Which carriage returns were line endings is decided from the parse rather than from the bytes: a lone carriage return is translated, and the translation is kept only where it recovers records the reader could not see. What counts as recovered depends on what the file already is.

A file with no newline anywhere is one record to every reader on this path, and no ordinary CSV is written that way - a file that ends its records with newlines has newlines in it. Nothing about it is ambiguous, so any record the translation recovers is a record the file holds. This includes a classic Mac file whose rows do not match its header: it is read as the several records it is, and then refused by the field-count check below, which is the honest answer where merging it into one row is not.

A file that already has newline-delimited records is the ambiguous one. A lone carriage return in it is as likely to be a break inside a cell, and splitting that cell adds a record exactly as a real line ending would. There the records are counted by the header's width, because a record recovered from a line ending has the columns the header declares and a fragment torn out of one cell does not. A carriage return that recovers nothing therefore stays what it is, a line break inside a cell, and nothing untrue is said about the file's lines - except in the one shape below, where the two readings cannot be told apart at all.

One shape stays ambiguous and is decided rather than known: a single-column file with an unquoted carriage return in a value. Every fragment of a one-column record has the header's width, so field counts cannot separate the two readings, and the carriage return is taken as a line ending. The correction says what it did and the version before it is still there. A spreadsheet writing a multi-line value into a one-column file quotes it, and a quoted one is read as the cell break it is.

Correcting the file

The refusal offers the correction, and taking it is one control in the portal or repair=true on the tool call. What it does is a decode and a re-emit:

  • the bytes are read as UTF-8, or as windows-1252 when they are not valid UTF-8, and a leading byte-order mark is dropped
  • a carriage-return line ending becomes a newline, so each record is on its own line
  • each run of line breaks inside a cell becomes a single space, and the cell is trimmed
  • the file is written back out as UTF-8 CSV with no byte-order mark

The result is a new version of the file itself, written through the version trail its kind already has: a revision for a managed resource, a version for a portal asset. Nothing is copied to a second object beside the original, no mirror is kept, and the file as it was uploaded stays as the version before the correction - visible in the version panel and restorable from it. The registration is then built over the new version's directory, so the table is current from the moment it exists.

The result says what changed, in the portal and in the tool's answer, because the person's file changed. The same sentence is recorded on the version the correction wrote, for either kind, so the version panel says why the file changed without a reader having to find the registration that did it.

The choice is kept on the registration. A source that produces the same defect every time it runs - a weekly spreadsheet export, a script writing a CSV whose text fields carry paragraph breaks - would otherwise correct cleanly on the day somebody registered it and stop on every version after that. So repair is recorded on the registration, and a table that follows its file corrects each new version the same way: see Correcting the file as it follows. It only does anything for a following table, since a pinned one is never moved onto a new version and so never meets one to correct. The registration that is current holds the choice: registering the same name again without asking for the correction leaves a table that corrects nothing.

A file the correction cannot put right is refused once, and no correction is offered for it. Two things put a file there. A record whose field count differs from the header's is one: filling in a short record invents data and dropping a field from a long one loses some, and neither is a correction the platform can make on somebody's behalf. A file the reader cannot parse all the way through is the other: the correction rewrites every record, so it cannot be made over records it cannot read. Both are settled by the same read of the file that found the defect, so the refusal names the field counts, or the parse error, the first time it answers - rather than offering a correction that would then decline with a different problem. Either way the file has to be fixed where it was written.

Neither condition refuses a file on its own. A ragged CSV whose lines end in newlines and whose cells hold no line break registers as it always has, and so does one the reader gives up on partway: nothing here changes what happens to them.

Only a single-byte code page is converted. A spreadsheet's Unicode Text export is UTF-16, and every byte of a UTF-16 file is also a valid windows-1252 character, so reading one as a code page produces a character per byte - mojibake in every cell. A file whose bytes carry a UTF-16 or UTF-32 byte-order mark, or a NUL byte, is therefore refused outright and no correction is offered for it: re-export it as UTF-8 CSV and upload that. Nothing is offered that the platform cannot honestly do.

Nor is every file that is not UTF-8 a windows-1252 one. That code page leaves five of its 256 byte values with no character at all - 0x81, 0x8D, 0x8F, 0x90 and 0x9D - and a decoder asked to read one of them puts a replacement mark in its place and reports no error. A file holding one is in some other encoding, or is not text; converting it would write those marks into the person's new version and call it a conversion from windows-1252. So the code page is the answer only where every byte is one it defines, and a file carrying any of the five is refused with the same instruction: re-export it as UTF-8 CSV and upload that. The bytes on either side of them - 0x80, 0x8E, 0x9E and 0x9F - all have characters and still convert.

The NUL byte is looked for before the UTF-8 check rather than after it, because a NUL is itself valid UTF-8. The same Unicode Text export written without a byte-order mark, over content that is plain ASCII, is a valid UTF-8 file with a NUL beside every character; a UTF-8 check alone passes it, and the table it would register carries those NULs in its column names and in every cell.

Such a file is then reported on its encoding alone. The refusal says nothing about its line endings, how many of its rows are torn, or what its columns are called, because each of those readings would be taken from bytes read in the wrong encoding: a Windows line ending in a UTF-16 file puts a carriage return beside a NUL, which a reader of single-byte text takes for a line break inside a cell, and the column it would name is the run of NUL-laden bytes around it rather than a name the file holds.

Where the NUL is what identified the file, the refusal names the NUL rather than an encoding, since nothing else about those bytes says what they are.

A correction is written before the last of the checks have run, so a refusal - or a coordinator that would not run the statement - can arrive after the file has already changed. The answer says so in that case, and the audit event records the correction whether or not the registration it was for succeeded. The file stays corrected; registering it again creates the table.

A file that is already line-safe and valid UTF-8 with no NUL in it registers exactly as it always did. No version is written, nothing is rewritten, and the result says nothing extra - including when the correction was asked for and turned out to be unnecessary.

Following the file

A registration pins a directory: the directory the file's current version sat in when the table was registered. Every revision of a managed resource and every version of a portal asset writes the new content to a new directory and moves the head, so a registration left alone would go on reading the version that was current on the day it was made. A registration therefore follows its file by default: the write that produces a new version moves every following table over the file onto it, with the same statements a re-registration runs - drop, create at the new directory, columns re-read from the new version's header - under the same registrant, before the write returns. From the moment replace_content, the asset editor, or a script's platform.export answers, a query against the table reads the new contents. A version whose header differs gives the table the new columns.

The write says what happened to every table over the file. The result of manage_resource replace_content, of manage_asset content edits and reverts, of the portal's and the admin console's content routes, and of a script's platform.export carries one sentence per registered table: scratch.uploads.analyst_stores on scratch now reads version 7. for a table that followed, or ... is pinned to the version it was registered over and is now behind this file; register it again to move it ... for one that is not. A managed script's run log carries the same sentences, so a scheduled run that put a table behind its file says so in its history.

The write is never failed by the follow. The file changed; that write succeeded. A follow the coordinator refuses leaves the registration where it was, with the reason recorded on it, so the table is behind the file exactly as a pinned one is and the listing says why. A create that fails after the drop ran puts the table back at its old directory first. A new version that cannot be read as a table - a second file in its directory, a CSV that needs correcting - is refused the way a registration would refuse it, and the registration is reported behind with that reason. A follow rewrites the person's file only where the registration was made asking for exactly that: see Correcting the file as it follows.

Pinning is the choice. follow=false on the tool, "follow": false in the REST body, or the Follow the file box unticked in the portal registers a table that stays on the version of the file it was registered over. That is the right shape for a report that must keep returning the same rows until somebody decides otherwise. The choice is stored on the registration, shown as Pinned in Scratch Tables and on the registration's page, and re-settable by registering the same name again.

Changing the file, then, is four cases:

What happened What the table does What to do
The object is overwritten at the same key. A vendor drop replacing yesterday's file, a bucket destination a script writes to. The next query returns the new contents. Nothing.
A new version is written over a file whose table follows it. Every portal asset edit does this, as does every revision of a managed resource. The table is moved onto the new version before the write returns, and the write's result names it. Nothing.
A new version carrying a correctable defect is written over a file whose table follows it and corrects it. A corrected version is saved above it, under the registrant, and the table is moved onto that. The write's result names the table and the correction. Nothing.
A new version is written over a file whose table is pinned. The table keeps serving the directory it was registered against. That is correct SQL over the version that was current when it was registered. The write's result says the table is behind. Register again, same connection and name, when it should move.

The last case, and a follow that could not be completed, are reported as stale everywhere a registration is shown: in the portal panel, on a search hit, in Scratch Tables, and in manage_table action=list. A stale table is not broken and is not dropped, so a report built on it keeps running; it is behind, and the platform will not move a pinned table forward on its own.

A managed resource cannot be overwritten in place: every replace_content is a revision, which is what keeps a referenced file's history restorable. So a managed script keeping a table current writes the resource and lets the table follow, which is what a registration does unless it was pinned. A bucket destination a script delivers to is the shape that overwrites in place, and needs no registration to move.

Correcting the file as it follows

A table registered with repair corrects its file, and goes on correcting it. When a new version arrives carrying a defect of the kind the correction can put right - a carriage-return line ending, a line break inside a cell, a windows-1252 code page - the follow saves a corrected version of the file through the version trail its kind already has, and moves the table onto that version. The version that carried the defect stays below it, exactly as the uploaded bytes do after a register-with-repair: it is in the version panel, it can be downloaded, and restoring it writes those bytes back as a new version.

Restoring it does not leave them as the file's head. A restore is a new version like any other, so the follow meets it, finds the same defect, and corrects it again - which is what the registration is for. To keep the defective bytes as what the file serves, register the table again without asking for the correction, or with follow off.

The corrected version is written under the registrant, not under whoever made the write that triggered the follow, which is the identity a follow already runs and is audited under. Its change summary is the same sentence a register-with-repair records, so a person reading the file's history finds a version nobody typed with the reason it appeared beside it. The write's result says both halves: scratch.uploads.analyst_stores on scratch now reads version 7. Saved version 7 of this file, which put 94 rows back onto one line. ...

The registrant is an address, and that is all a registration keeps of them. So the corrected version records the registrant as its author, while what produced it is whatever the write that triggered the follow was produced by - the session or the script whose data the corrected version holds. Those are different questions with different answers, and the version panel and the provenance panel each show their own.

The file is corrected once for the version, not once per table over it. Where more than one registration over the file carries the choice, one corrected version is written - under the registrant of the oldest of them, the choice that has stood over this file the longest - and every following table lands on it, including any that did not ask for the correction: the corrected version is the file's head, and a following table reads the head. The correction is reported once, on the table whose registration the correction was made for - the oldest one carrying the choice. A table registered without repair that lands on the corrected version is told only that it moved, because it did not ask for anybody's file to be rewritten.

What was refused stays refused. A defect the platform will not correct - a wide encoding, a NUL byte, records that do not match the header, bytes the reader cannot parse through - leaves the registration behind with the reason it gives today, and no version is written. A registration made without repair is left behind the same way, with the sentence offering the correction, because nobody asked for its file to be rewritten. A pinned registration is not moved onto a new version at all, so carrying the choice does nothing for it.

When a table disappears

A follow, an unregister and a replacing registration each run DROP TABLE. Afterwards the write asks the connection whether every other registration on it still holds its table, records the ones that do not (follow_error opens with The table no longer exists), and says so: a follow reports it in the tables sentences beside the tables it moved (<table> on <connection> no longer exists: the table was removed while <other> was moved to version N. Register it again to restore it.), a replacing registration in its result, and an unregister on the registration rows the listing reads. A lookup the connection cannot answer is logged, not reported, since a connection that cannot answer has not said the table is gone. A table dropped outside the platform is noticed by the next write that runs DDL on its connection.

The case this exists for is an object store whose prefix listing does not stop at a directory boundary. Trino's file metastore drops a table by listing its metadata directory, and such a store answers the listing for x/ with x_pinned/... too, so dropping x takes x_pinned's metadata with it. A deployment on a file metastore should run an object store that honors the boundary (SeaweedFS before 4.17 does not); the check above is what keeps a registration honest either way.

Querying what you registered

Every column of a registered table is VARCHAR. That is the Hive CSV storage format's rule, not a platform choice - declaring the table any other way is refused by Trino itself - so a join to a typed warehouse column needs a cast:

SELECT s.store_id, s.store_name, u.rebate_pct
FROM warehouse.public.stores s
JOIN scratch.uploads.analyst_vendor_keys u
  ON s.store_id = CAST(u.store_id AS integer)

search carries a table reference on a hit for a registered file, and fetch carries a tables list on the record: one entry per registration over the file, newest first, each with a sample statement showing the cast and with the same registration_id, follow, repair and follow_error the listing reports. A file registered twice - on two connections, or registered again after a header change - is two entries, and the document names both.

The hit carries one, because a hit is a pointer to somewhere the data can be queried rather than an inventory: it is the newest registration whose follow_error is empty, so a hit never points at a table a follow has already reported gone. A file whose every registration carries a follow error gets no table on its hit, and its fetch document is where the reasons are.

Those are the same references manage_table takes, so finding a file and registering it are one turn apart.

What is refused, and why

A directory holding a file Trino would read. Trino reads every non-hidden object under an external location and parses it as CSV. A stray file beside the content does not fail the query - it comes back as rows of that file's bytes. Registration therefore refuses a directory with such a sibling and names it. A name beginning with . or _ is hidden: Trino skips it, and so does the check. Thumbnails the portal captures are written under those names, so a CSV asset that has been rendered in the portal registers with its thumbnails in place.

Thumbnails captured before those names were adopted are stored as thumbnail.png and thumbnail_dark.png - ordinary files that Trino does read, so an asset still carrying them is refused. An asset carrying one is on the portal's thumbnail refresh queue for exactly this reason, so opening the portal in any tab captures it again under the hidden names and removes the objects it supersedes, after which it registers. Nothing has to be run against the bucket.

A file whose own name Trino skips. The same ./_ rule applies to the file being registered. A table over a hidden object is created, recorded and queried without any error and returns nothing, so a source under such a name is refused and the reason is stated. Upload the file under another name.

A file that is not a CSV. There is no header row to take column names from.

A CSV a line-based reader cannot read. Lines that end in a carriage return rather than a newline, a line break inside a cell, or bytes that are not readable as UTF-8 text - which a NUL byte makes them, whether or not the rest of the file is valid UTF-8. See A CSV a query engine cannot read.

A file that is not yours. Registering is the authority to change the file, not the authority to read it. An asset is registrable by its owner or an administrator; a resource by its uploader, or by whoever may write to the scope it lives in - a platform administrator, or the administrator of that persona. That is the same rule that governs updating and deleting each kind, and it holds on every surface: the portal panel, the REST routes and manage_table resolve the file through one resolver per kind.

The rule is authority to change rather than authority to read because registering publishes the file's contents into a schema everyone granted the connection can read, and resource scopes are not carried into Trino. A read rule would let anyone who can see a persona-scoped file widen its audience.

A connection your persona is not granted. Registration meets the same connection boundary a tool call meets.

A read-only connection. The DDL runs through the same read-only check trino_execute runs, so a connection with read_only: true refuses it.

Dropping a table you did not register. Unregistering is the registrant's call, or an administrator's: the table lives in a schema everyone with the connection shares, and the person who put it there is the one who takes it out. Deleting the file itself drops every table over it, whoever registered them.

A table name someone else registered. The scratch schema is shared, so the name is claimed on registration. Re-registering your own name replaces it; taking someone else's is refused, naming who holds it. Administrators are unrestricted.

What the scratch schema is

The scratch schema is a shared workspace. Everyone granted the connection can read every table in it, and resource and asset permissions are not carried into Trino - see the threat model.

Table names are prefixed with the registering person's persona. That is a collision-avoidance measure and a legibility one, not a boundary: it keeps an analyst's vendors apart from an administrator's, and it tells a reader of the schema whose working table they are looking at.

The prefix separates personas rather than people, so two analysts registering vendors do land on the same name, and the prefix is not what decides which of them gets it. The name is claimed in a unique index on the connection, catalog, schema and table, and the record of who registered it is what the platform checks: re-registering a name you hold replaces the table, and a name somebody else holds is refused rather than overwritten. Administrators are unrestricted, so an administrator does replace another person's table.

What keeps a registration off the warehouse is the Trino identity the scratch connection authenticates as. The platform's read_only flag is a statement-prefix denylist evaluated per connection; nothing in the toolkit restricts a catalog or a schema, and catalog/schema on a connection are session defaults rather than bounds. A scratch connection that authenticates as the same Trino user as the warehouse connection can write to the warehouse. Give it its own Trino user, with access-control rules allowing DDL only on the scratch catalog.

What is recorded

Every registration and unregistration writes an audit event: who, which connection, the statement that ran, and the table it named. Failed attempts are recorded too. A registration that corrected the file first records what the correction changed on the same event, because it rewrote somebody's file on their behalf - and it records it even when the registration went on to be refused, since the file changed either way.

Every follow writes a table_follow event under the registrant - the person whose table moved, not the caller of the write that moved it - naming the statements that ran, the version the table followed to, and the columns before and after when the header changed. A follow that failed is recorded with the failure, and so is the statement that put the table back.