The mapping is the schema, and selecting everything is not
An index that maps container fields answers a select-everything statement with no columns at all, so the schema tree is built from the mapping instead.
Point a schema tree at an index and the obvious way to learn its columns is to ask
the engine for them. Here that is the wrong way round. The Elasticsearch SQL schema
columns come from the index mapping, not from a statement, because a statement only
ever describes itself while the mapping describes the index. The provider reads
GET /_cat/indices?format=json&bytes=b for the indices and GET /<index>/_mapping
for the fields, and never builds a SELECT to find out what exists.
That decision was forced by a measurement.
What a select-everything statement returns on a mapped index
Measured on Elasticsearch 9.1.4, against an index whose mapping declares one
flattened field and one nested field, POST /_sql?format=json answers a
select-everything statement like this:
{ "columns": [], "rows": [[]] }
A table with no columns and one empty row. Nothing failed. The statement was accepted, executed and answered; it is simply that the projection this grammar expands does not include a container, so there was nothing left to declare. A tree built from that answer would render the index as having no fields, which is a fabricated schema and the worst of the available outcomes: it looks like data.
Two smaller traps sit next to it. A container is not merely skipped, it breaks the
whole statement - SELECT address FROM probe_shapes is HTTP 400,
verification_exception, “line 1:8: Cannot use field [address] type [object] only
its subfields”, while SELECT note, note.keyword, address.city is HTTP 200 for all
three. And a trailing semicolon is a syntax error on this grammar
(“extraneous input ‘;’ expecting LIMIT 50 and nothing further.
DESCRIBE would answer better than SELECT * does, but it is still a SQL surface,
and the availability of the SQL surface is the one thing an object browser must not
depend on. The mapping is readable on a closed index, measured. A statement is not.
Where the columns come from: the mapping, flattened to dotted paths
The flattening rule is not invented. It is copied from this product’s own
DESCRIBE, measured on a probe index holding an object field and a multi-field:
| field | SQL type | mapping type |
|---|---|---|
address |
STRUCT |
object |
address.city |
VARCHAR |
keyword |
note |
VARCHAR |
text |
note.keyword |
VARCHAR |
keyword |
Containers appear, leaves appear, and a multi-field appears as a child. The
provider’s flattener descends both properties (objects) and fields
(multi-fields) and reproduces exactly that set. Nothing outside properties is
read, because a mapping carries siblings such as _meta that are metadata about
the mapping rather than fields in it.
Then one subtraction. object and nested are dropped from the column list,
because the starter query enumerates every declared column and a container makes
the whole statement fail. A text field with a keyword sub-field is kept, since
both SELECT note and SELECT note.keyword answer 200 - so “has sub-fields” is
the wrong test and the field’s own type is the right one.
Columns are sorted by path, by code unit. This engine happens to normalize mapping
properties alphabetically even for a dynamically mapped index, but that is its
normalization and not a promise, and a mapping has no declaration order to preserve
in the first place because documents are unordered JSON. Sorting by path also keeps
address.city beside its siblings once the container above it is gone.
The tree makes one mapping read per index, at most four at a time. A per-index failure costs one index’s columns rather than the tree, and only for two causes: a permission refusal, because a security plugin grants index privileges per index and a role that lists twenty indices and may describe nineteen is an ordinary configuration, and a missing index, because an index deleted between the listing and the mapping read is a race. Everything else propagates. An unreachable cluster rendering every index with zero columns would read as “these indices have no fields”, which is the same fabricated schema in a different costume.
Types in the mapping vocabulary, not SQL names
The result grid labels columns with the mapping’s own type names. Measured,
SELECT customer, total FROM probe_orders declares keyword and double, not
VARCHAR and DOUBLE, and SELECT note declares text.
That is the same vocabulary the mapping read reports, so the tree and the grid speak one language: the word beside a field in the sidebar is the word above the column in the results. It is also the vocabulary the user wrote themselves, in the document they actually edit, which is the mapping.
A column whose declaration carried no type name is left out of the type list rather than given a placeholder. An invented type would be indistinguishable from one the engine sent.
Why no column is primary and every column is nullable
No column is ever reported as primary and every column is nullable, because
nothing a mapping declares is unique. Indexing the same document body twice
yields two documents. The only unique thing in an index is _id, which is
metadata rather than a mapped field and is not even selectable through this SQL
surface - measured,
SELECT _id FROM probe_orders answers verification_exception, “Unknown column
[_id], did you mean [id]?”.
nullable: true is a measurement, not a hedge. A mapping declares how a field is
indexed if a document carries it. There is no NOT NULL in the model, and a
document indexed without note really does come back as null. A mapping’s
null_value is the closest thing to a default and is not one: it is the term
substituted into the index so an explicit null becomes searchable, and it changes
no value any document carries.
The primary-key flag is worth stating plainly because of where it is read. It is consumed as fact: completions append “(PK)” to the field name, the agent’s schema context puts “ PK” into what a model reasons from, and schema diffing reports “Primary key changed”. A key invented in the introspector becomes a key the product asserts, in a prompt, to a model that will then write a statement around it.
Foreign keys are always empty and the provider declares that it does not declare them, so the empty list means “impossible here” rather than “none found”. Secondary indexes are always empty and the index count is zero and stays zero: every mapped field is inverted-indexed as a property of being mapped, so there is no index object to name.
What the tree deliberately leaves out
Aliases and data streams are not listed in the sidebar, even though SQL statements
accept them. They come from _alias and _data_stream, and the transport seam
carries neither, so a perfectly queryable alias can be typed into the editor and
run while never appearing in the tree. That is a gap, stated rather than papered
over.
Dot-prefixed system indices are hidden by default. A closed index is kept, with its row count and size omitted rather than zeroed, because the listing returns JSON null for both while the mapping still answers in full - dropping the index would report it as gone when it is merely closed.
One number reads differently from what the word suggests. An index whose mapping
has nested fields reports more documents in the monitoring panels than a
SELECT COUNT(*) returns in the editor, because every nested element is stored as
its own document.
None of this is a browser waiting for write support. This grammar has no INSERT,
UPDATE, DELETE, CREATE TABLE or ALTER TABLE - each one is HTTP 400 with the
parser listing everything it would have accepted - so inline row editing and the
create-table toggle are not offered rather than offered and failed, which is the
same rule that governs
every capability this interface shows or hides.
A grammar that cannot write is still not the guarantee agent auto mode asks for. Auto mode runs only where the provider implements a database-native read-only profile, which is PostgreSQL, SQLite and DuckDB and no other entry in the supported engine list; on this engine an auto run ends engine-unsupported. Plan mode opens on the connection, is toolless, runs nothing, and drafts a statement for a human to run.