# Technical Documentation: xo/DBMETA

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/xo/DBMETA)

### Overview (`README.md`)

<div align="center">
  <a href="#about" title="About">About</a> |
  <a href="#installing" title="Installing">Installing</a> |
  <a href="#using" title="Using">Using</a> |
  <a href="#database-support" title="Database Support">Database Support</a> |
  <a href="#version-support" title="Version Support">Version Support</a> |
  <a href="#design" title="Design">Design</a> |
  <a href="#contributing" title="Contributing">Contributing</a>
</div>

<br/>

[![Unit Tests][dbmeta-ci-status]][dbmeta-ci]
[![Go Reference][goref-dbmeta-status]][goref-dbmeta]
[![Releases][release-status]][releases]
[![Discord Discussion][discord-status]][discord]

[dbmeta-ci]: https://github.com/xo/dbmeta/actions/workflows/test.yml "Test CI"
[dbmeta-ci-status]: https://github.com/xo/dbmeta/actions/workflows/test.yml/badge.svg "Test CI"
[goref-dbmeta]: https://pkg.go.dev/github.com/xo/dbmeta "Go Reference"
[goref-dbmeta-status]: https://pkg.go.dev/badge/github.com/xo/dbmeta.svg "Go Reference"
[release-status]: https://img.shields.io/github/v/release/xo/dbmeta?display_name=tag "Latest Release"
[releases]: https://github.com/xo/dbmeta/releases "Releases"
[discord]: https://discord.gg/WDWAgXwJqN "Discord Discussion"
[discord-status]: https://img.shields.io/discord/829150509658013727.svg?label=Discord&logo=Discord&colorB=7289da&style=flat-square "Discord Discussion"

# About

`dbmeta` reads database metadata. Metadata means the description of what a
database contains: its schemas, tables, columns, indexes, constraints,
functions, types, roles, privileges and the rest.

One API answers for every database. A caller asks for tables without knowing
which database answers, and receives the same types whichever one does.

`dbmeta` reads. It does not change a database and it does not render results.
It is used by [`usql`][usql], a command line client for many databases, and by
[`dbtpl`][dbtpl], a code generator.

PostgreSQL is the model. The shape of every answer follows the metadata
commands of [`psql`][psql], because bringing that experience to other databases
is the purpose of this package.

# Installing

```sh
go get github.com/xo/dbmeta
```

`dbmeta` depends on the standard library and on nothing else. It imports no
database driver and no URL parser. The caller opens the connection with the
driver of its choice and passes it in.

Nothing here parses a connection string, because nothing here opens a
connection. If you want a URL to open a database, use [`dburl`][dburl], which
is what the examples below do. `dbmeta` never sees it.

# Using

Import the package and the models for the databases you need:

```go
import (
    "github.com/xo/dbmeta"
    _ "github.com/xo/dbmeta/all"
)
```

`dbmeta` asks a database to do one thing, and says so in the type system:

```go
type Queryer interface {
	QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
}
```

`sql.DB`, `sql.Tx` and `sql.Conn` all satisfy it. Pass a `sql.Tx` to read
several catalogs in one snapshot.

Open a connection, read the version, then ask:

```go
db, err := dburl.Open("postgres://localhost/example")
if err != nil {
    return err
}

// dbmeta supplies the version statement, runs it against the connection you
// passed, and parses the answer.
versions, err := dbmeta.PostgreSQL.Version(ctx, db)
if err != nil {
    return err
}

m, err := dbmeta.New(dbmeta.PostgreSQL, versions)
if err != nil {
    return err
}

for t, err := range dbmeta.Tables.All(ctx, m, db, dbmeta.Args{Schema: "public"}.Map()) {
    if err != nil {
        return err
    }
    fmt.Println(t.Schema, t.Name, t.Type)
}
```

There is one query value per kind of object, such as `dbmeta.Tables` and
`dbmeta.Columns`. Name the value and the result type follows from it.

`dbmeta.Open` does the version and `New` in one step, and returns the error
when the version cannot be read. `Query.Each` takes an `Args` rather than a
map and passes only the arguments the query takes, so one `Args` serves every
kind of object. `All` refuses an argument the query does not take, which
catches a misspelled name. `Dialect.Pattern` turns a psql pattern, such as
`public.film*`, into a schema pattern and a name pattern, folded as the
product folds a name. See D169.

```go
m, err := dbmeta.Open(ctx, dbmeta.PostgreSQL, db)
if err != nil {
    return err
}
schema, name := dbmeta.PostgreSQL.Pattern("public.film*")
for t, err := range dbmeta.Tables.Each(ctx, m, db, dbmeta.Args{Schema: schema, Name: name}) {
    if err != nil {
        return err
    }
    fmt.Println(t.Schema, t.Name, t.Type)
}
```

## Running the statement yourself

A caller that prints statements, or that runs them its own way, asks for the
statement instead of the rows:

```go
sqlstr, args, err := dbmeta.Tables.Build(m, dbmeta.Args{Schema: "public"}.Map())
```

`Query.Fields` and `Query.Params` describe what a query returns and what it
takes. `dbmeta.Queries` lists every query, and `Query.Support` says whether
this database answers it.

## Overriding the version

The caller chooses the version. `dbmeta` never detects one behind your back.
Build a version set by hand and pass it to `New` when a proxy hides the server,
when a compatible product reports a version it does not behave like, or when
there is no server at all.

## Iterators hold a connection

An iterator holds a database connection until it ends. Stopping early, with
`break` or by canceling the context, releases it.

Do not open a second iterator inside the body of the first. That needs a second
connection and deadlocks on a pool of one. Ask for every row you want in one
call and filter in the loop.

# Database Support

| Database   | Model              | Queries | Status      |
| ---------- | ------------------ | ------- | ----------- |
| PostgreSQL | native             | 65      | Complete    |
| any with an information_schema | shared | 12 | Ready to build on |
| MariaDB    | native             | 30      | Complete    |
| MySQL      | native             | 27      | Complete    |
| SQLite3    | native             | 14      | Complete    |
| DuckDB     | native             | 21      | Complete    |
| SQL Server | native             | 34      | Complete    |
| Oracle     | native             | 29      | In progress |
| Cassandra  | native             | 17      | In progress |
| ScyllaDB   | native             | 18      | In progress |
| ClickHouse | native             | 25      | In progress |
| Trino      | native             | 13      | In progress |
| Presto     | native             | 9       | In progress |
| Firebird   | native             | 25      | In progress |
| SAP HANA   | native             | 33      | In progress |
| Apache Hive | native            | 18      | In progress |
| Exasol     | native             | 26      | In progress |
| Vertica    | native             | 28      | In progress |
| Couchbase  | native             | 12      | In progress |
| CockroachDB | native            | 54      | In progress |
| CrateDB    | native             | 27      | In progress |
| QuestDB    | native             | 11      | In progress |
| TiDB       | native             | 20      | In progress |
| Vitess     | native             | 21      | In progress |
| Databend   | native             | 20      | In progress |
| SingleStore | native            | 23      | In progress |
| Snowflake  | native             | 15      | In progress |
| Amazon Redshift | native        | 18      | In progress |
| Apache Impala | native          | 11      | In progress |
| rqlite     | native             | 14      | In progress |
| libSQL     | native             | 14      | In progress |
| InfluxDB 3 | native             | 9       | In progress |
| Neo4j      | native             | 17      | In progress |
| YDB        | native             | 7       | In progress |
| ArangoDB   | native             | 7       | In progress |
| Apache Avatica | native         | 24      | In progress |
| Apache Druid | native           | 7       | In progress |
| Apache Drill | native           | 10      | In progress |
| Elasticsearch | native          | 8       | In progress |
| OpenSearch | native             | 3       | In progress |
| Apache Solr | native            | 4       | In progress |
| GizmoSQL   | native             | 21      | In progress |
| Spanner    | native             | 22      | In progress |
| Google BigQuery | native        | 18      | In progress |
| Amazon Athena | native          | 8       | In progress |
| Databricks | native             | 17      | In progress |
| Azure Cosmos DB | native        | 15      | In progress |
| InfluxQL   | native             | 7       | In progress |
| SurrealDB  | native             | 18      | In progress |

A native model reads the catalog the database keeps for itself. A shared model
reads `information_schema`, which is a smaller answer that many databases have.
It answers 12 object kinds where the native PostgreSQL model answers 65, and
answers none of them completely: no size, owner or access method for a table,
no storage or index detail for a column, no exclusion constraint, no aggregate.

ClickHouse ships an `information_schema` and the model does not read it. It is
an emulation that reports what the standard names and drops what makes a
ClickHouse table what it is: the engine, the partition key, the sorting key,
the codec per column and the skipping indices. `system` has all of it.

Cassandra has no `information_schema` at all and has a native model for the
same reason SQLite and Oracle do. It is the only one here that is not
SQL, and CQL is narrower than the name suggests: it cannot compute, it cannot
express an optional filter, and it cannot order across partitions. D62 holds
what follows from that. `usql` builds on the shared reader today for
Snowflake, Trino, Databend and DuckDB, which is the evidence for who
the shared model serves.

SQL Server was on that list too. `usql` reads it through the shared reader with
sequences and constraints switched off and a small plugin for catalogs and
indexes, and the native model here answers 32 kinds instead, including the
sequences and constraints that reader turns off.

[`COVERAGE.md`](docs/COVERAGE.md) says what each database answers, what it cannot,
and which analogues were found and rejected. MariaDB answers 30 of the 65 and
MySQL answers 27, because a native model beats the shared one by seventeen.

MariaDB and MySQL share one model. A query written for one of them gates on the
product rather than on the release number, because MariaDB is at 13.0 and MySQL
at 26.7 and neither number says anything about the other. CI runs both products
and a third job compares them against the same schema.

Cassandra and ScyllaDB share one model in the same way. ScyllaDB answers one
kind more, `RoleSettings`, from the service level attached to a role. D91
records how the model tells the two products apart.

SQLite and DuckDB have no server. Both are libraries, so the release under test
is whichever one the Go driver was built with, neither needs a container, and
neither model carries a version gate.

Every driver the tests use is the one the `dburl` registry names for that
dialect, because dburl is upstream of `dbmeta` and of every consumer. The
version can differ and the package must not, because a query that works here
and fails on the driver a consumer opens is a query that does not work. See D52
and D154.

A model ships its queries and a fixture together. The fixture is a known good
schema containing one of every object the queries read, exported so that other
projects can generate against it.

# Version Support

Every version sits in one of five tiers. Read the tier before you rely on a
version.

| Tier     | What it means                                                      |
| -------- | ------------------------------------------------------------------ |
| Tested   | Tests run on every change, in CI                                     |
| Nightly  | Tests run once a night, in CI                                        |
| Verified | Tests run on a development machine before a release, and not in CI   |
| Staged   | dbrun starts it, and it was measured when it was added. No model reads it yet, so nothing runs it in CI |
| Archived | The queries exist and were checked once, and nothing runs them now   |

D40 set three of these, D42 added Nightly and D119 added Staged. The first four
are values of `container.Tier`. Archived is not, because an archived release is
one the list does not name at all.

A Staged release is not supported by dbmeta. It is there so that dbrun can
start a server for a sister project, such as dbimp's drivers, or for a flavor
that a model does not detect yet. It moves to Tested, Nightly or Verified in
the change that adds its model, and takes as its tier the cadence it already
records: Tested, Nightly or Verified. `dbrun list --json` prints the cadence,
and dbimp runs the tested ones on each push and the nightly ones at night
(D120).

## PostgreSQL

Supported from release 9.6 to release 18, which is ten major versions: 9.6, 10,
11, 12, 13, 14, 15, 16, 17 and 18.

| Releases          | Tier     |
| ----------------- | -------- |
| 9.6, 12, 15, 18   | Tested   |
| 10, 11, 13, 14, 16, 17 | Nightly  |

Four releases are Tested: CI starts a real server for each and runs the
integration tests on every change. They are the floor, the ceiling and one on
each side of the middle, which is the smallest set that catches every fault
found so far. Testing only the newest catches two of the six.

The other six are Nightly, and CI runs them once a night. `dbrun` runs any of
the ten on a development machine.

Every query is executed against a real server at all ten releases by `dbrun`,
which also checks that the columns returned match the fields
declared and that a field the server is too old for arrives as NULL. The
objects PostgreSQL did not have before release 10 are refused there rather than
returning an empty result:
publications, publication tables, subscriptions, extended statistics and
partitioned tables.

`psql` itself dropped support for servers below release 10 in PostgreSQL 20.
`dbmeta` supports 9.6 deliberately, and its queries for that release are
translated from an older checkout.

## SQL Server

Supported on 2017, 2019, 2022 and 2025 in containers, and on 2008R2, 2012,
2014 and 2016 on Windows machines.

| Releases                     | Tier     |
| ---------------------------- | -------- |
| 2017, 2019, 2022, 2025       | Tested   |
| 2008R2, 2012, 2014, 2016     | Verified |

All four are Tested, and CI starts a real server for each on every change.
There are only four, and every version gate the model carries sits below all of
them, so there is no older branch that a smaller matrix leaves uncovered.

2017 is the first release with a Linux container. Microsoft shipped SQL Server
on Linux from 2017, so the older releases run on Windows machines that `dbrun`
builds. They are Verified: a person runs them on a development machine before
a release, and CI never does. The queries carry gates for `sys.sequences` and
`sys.dm_db_stats_properties` at 2012 and for `sys.external_tables` at 2016,
and `models/sqlserver/version_test.go` checks that those gates resolve. See
D54 and D57.

# Design

`dbmeta` supplies the data and the consumer decides what to show. `psql` sets
the object model and it does not set the column set, so a query here returns
facts `psql` does not print where the database can produce them in the same
statement. D47 holds the rule and the cost test.

Read [`NULLS.md`](docs/NULLS.md) before writing a query for any database. It is
the shortest document here and the one that cost the most to learn.

Everything else is in [`docs/`](docs/):

| Document | What it holds |
| --- | --- |
| [`PLAN.md`](docs/PLAN.md) | The plan: the purpose, the architecture, what exists, the testing plan and the open questions for Ken. |
| [`decisions/`](docs/decisions/README.md) | Every decision, 236 of them, one file each, with the reasoning and what was rejected. The index lists them with their status, because 97 amend or replace an earlier one. |
| [`NULLS.md`](docs/NULLS.md) | One rule: never collapse a NULL. |
| [`COVERAGE.md`](docs/COVERAGE.md) | What each database can and cannot answer, per object kind, and which analogues were rejected and why. |
| [`COMMANDS.md`](docs/COMMANDS.md) | Every `psql` metadata command mapped to the Go value that answers it, which is what wiring up a client needs. |
| [`QUERIES.md`](docs/QUERIES.md) | What `psql` describes, what `information_schema` describes, and where the two meet. |
| [`DIALECT.md`](docs/DIALECT.md) | Every step needed to add a database, in order, with the test that catches each one you skip. |
| [`EVALUATION.md`](docs/EVALUATION.md) | How the supported version range is chosen, and how to choose one for a database not covered yet. |
| [`USQL.md`](docs/USQL.md) | What `usql` answered for each of the 47 drivers it built at the commit measured, and what changes if it reads `dbmeta`. |
| [`DBTPL.md`](docs/DBTPL.md) | The same measurement for `dbtpl`. |
| [`DBRUN.md`](docs/DBRUN.md) | How to use `dbrun`, the command that starts the databases the tests run against, and the rules for sharing one machine. |
| [`CONTAINERS.md`](docs/CONTAINERS.md) | How to add a container or a virtual machine that `dbrun` can start. |
| [`BACKLOG.md`](docs/BACKLOG.md) | Work that is known and not done, with the decision or the measurement that found each item. |
| [`PROGRESS.md`](docs/PROGRESS.md) | Where the work stands, so that a session that ends or crashes can resume. |
| [`WINDOWS.md`](docs/WINDOWS.md) | The Windows machines that host the SQL Server releases with no Linux container, and why each of them is awkward. |

[`AGENTS.md`](AGENTS.md) holds the rules for writing code here, for a coding
agent, with a table saying which document to read for which task.
[`CLAUDE.md`](CLAUDE.md) imports it, so that Claude Code reads the same rules.
[`CONTRIBUTING.md`](CONTRIBUTING.md) is the same for a person, and shorter.

# Testing

`container/` names every server release that `dbrun` starts, as Go data, one
file per product, and `container.All` joins them. Many are there only for
dbimp's drivers, for the flavors usql reaches, or as emulators of hosted
services, and no model reads them. Those are Staged, and CI does not run them
(D119). Each entry holds the image, the tag, the environment, the readiness
command and the connection string, and it starts nothing: a caller brings its
own podman, docker or Go client, and `dbmeta` depends on none of them.

```go
for _, s := range container.All() {
	fmt.Println(s.Name(), s.Tier, s.Ref())
}
```

Run the tests against a release, or a product, or a tier:

```bash
cd test && go run ./cmd/dbrun test mariadb-13.0
```

`dbrun test all` runs every release. It reads the list from `container`, starts
each server, waits for it to accept a connection, runs the tests and removes
the container. CI builds its matrix from the same list, and
`container/workflow_test.go` fails when the two disagree. Nothing else starts a
container, which is D68. `docs/DBRUN.md` says how to use it.

Every database also answers one checked in expectation. `TestConformance`
builds the same core schema on every database with a model and compares the
portable facts, so a difference between two families is either fixed or written
down. The relational databases agree on at least 23 of the canonical lines, and
every database agrees on at least 4,
and [`COVERAGE.md`](docs/COVERAGE.md) records every difference that is not.

# Related Projects

`dbmeta` is one of a set of packages that each do one part of the job, so that
a client can take the parts it needs.

- [`usql`][usql] is a command line client for many databases. It is the reason
  the object model follows `psql`. [`USQL.md`](docs/USQL.md) measures what it
  answers today and what `dbmeta` changes.
- [`dbtpl`][dbtpl] generates Go code from a database schema. It reads the same
  metadata and it can build against the fixtures here. [`DBTPL.md`](docs/DBTPL.md)
  measures the same thing for it.
- [`dburl`][dburl] parses a database URL and opens a connection. `dbmeta` never
  parses one, and it never repeats the scheme and flavor taxonomy that `dburl`
  holds. From v0.29.0 `dburl.Scheme` describes each scheme as well as parsing
  it, with the Go driver package, whether that driver needs cgo, and whether
  the product is embedded, a server or hosted. That is where to look up a
  driver, and D80 says why `dbmeta` reads it as a document rather than
  importing it.
- [`tblfmt`][tblfmt] renders a result set the way `psql` does. `dbmeta` reads
  metadata and does not render it, so a client that wants a table passes the
  rows to `tblfmt`.

# Contributing

Read [`AGENTS.md`](AGENTS.md) first. It holds the rules, including the ones
that are not obvious: the standard library only, no cgo in anything a consumer
builds, no build constraint on an operating system or an architecture, and a
context on every function that reads from a database.

Run the checks before you send a change, with the same flags CI uses:

```sh
gofmt -l . && go vet ./... && go build ./... && go test -race -count=2 ./...
```

To run the integration tests, start a server and point the test module at it:

```sh
cd test && go run ./cmd/dbrun test postgres-18
```

`dbrun test tested nightly verified` runs every supported release of every
product, which is what has to pass before a release. `dbrun test all` adds the
Staged releases, which no model reads.

Tests in this module never open a database connection. They render statements,
resolve versions, and read rows from a fake driver replaying recorded data, so
they need nothing installed and they cover releases whose container images no
longer start.

<br/>

<div align="center">
  <a href="https://github.com/xo/usql" title="A command line client for many databases">usql</a> |
  <a href="https://github.com/xo/dburl" title="Database connection URLs">dburl</a> |
  <a href="https://github.com/xo/dbmeta" title="Database metadata, this project">dbmeta</a> |
  <a href="https://github.com/xo/dbimp" title="Database drivers in pure Go">dbimp</a> |
  <a href="https://github.com/xo/cassandra" title="A database/sql driver for Cassandra">cassandra</a> |
  <a href="https://github.com/xo/dbtpl" title="Go code generated from a database">dbtpl</a> |
  <a href="https://github.com/xo/tblfmt" title="Tables of database results">tblfmt</a> |
  <a href="https://github.com/xo/rline" title="The line editor of usql">rline</a> |
  <a href="https://github.com/xo/transit" title="tree-sitter in pure Go">transit</a>
</div>

[usql]: https://github.com/xo/usql "usql"
[dbtpl]: https://github.com/xo/dbtpl "dbtpl"
[dburl]: https://github.com/xo/dburl "dburl"
[tblfmt]: https://github.com/xo/tblfmt "tblfmt"
[psql]: https://www.postgresql.org/docs/current/app-psql.html "psql"

---

### Overview (`docs/decisions/README.md`)

# Decisions

Every decision this project made is a file in this folder, one per decision,
named by its number and its title. This table is the index. Find the number
here, then open the file.

Each file opens with its status. "Decided" means Ken chose it. "Proposed"
means an agent or a peer session suggested it and Ken has not confirmed it.
"Open" means nobody has chosen yet. A decision that changes an earlier one
says so in its status, as "Amends D50", and the earlier one says it back, as
"Amended by D111". Read the status before the decision.
97 of them amend or replace an earlier one, and a decision read without its
amendment is worse than no decision.

A new decision gets the next number and a file of its own. Add its row here.
`TestTheDecisionIndexIsComplete` fails when a decision has no row or a row is
wrong, and it prints the row to add. D111 moved the decisions here from one
file.

| # | Decision | Status |
| --- | --- | --- |
| [D1](D001-the-module-centralizes-database-metadata.md) | The module centralizes database metadata | Decided |
| [D2](D002-models-are-one-package-per-driver-under-models.md) | Models are one package per driver under `models/<driver>` | Amended by D71 |
| [D3](D003-the-root-package-is-the-driver-agnostic-api.md) | The root package is the driver agnostic API | Decided |
| [D4](D004-keep-the-object-coverage-drop-the-reader-naming.md) | Keep the object coverage, drop the Reader naming | Decided |
| [D5](D005-dbmeta-only-reads.md) | dbmeta only reads | Amended by D56 |
| [D6](D006-fix-the-null-scan-defect-once-and-never-hide-a.md) | Fix the NULL scan defect once, and never hide a NULL | Decided, amended in place, D6a superseded by D71 |
| [D7](D007-use-the-standard-library-third-party-packages.md) | Use the standard library. Third party packages are a last resort | Decided |
| [D8](D008-version-differences-are-generated-data-not.md) | Version differences are generated data, not packages | Decided |
| [D9](D009-there-are-two-platonic-models-postgresql-is-the.md) | There are two platonic models. PostgreSQL is the primary one | Decided |
| [D10](D010-take-the-initial-design-from-dbtpl-and-its.md) | Take the initial design from dbtpl and its models directory | Decided |
| [D11](D011-dbtpl-is-pinned-as-a-tool-in-the-generation.md) | dbtpl is pinned as a tool, in the generation module | Amended by D26, superseded by D71 |
| [D12](D012-work-against-live-databases-running-in.md) | Work against live databases running in containers | Amended by D70 and D71 |
| [D13](D013-build-the-models-before-the-root-package.md) | Build the models before the root package | Decided |
| [D14](D014-a-driver-is-a-family-not-a-product.md) | A driver is a family, not a product | Decided |
| [D15](D015-ci-runs-on-github-actions-on-ubuntu-latest-only.md) | CI runs on GitHub Actions, on ubuntu-latest only | Decided |
| [D16](D016-user-facing-text-follows-the-simple-english.md) | User facing text follows the simple English rules | Decided |
| [D17](D017-every-metadata-read-takes-a-context.md) | Every metadata read takes a context | Decided |
| [D18](D018-the-whole-package-is-idiomatic-go.md) | The whole package is idiomatic Go | Decided |
| [D19](D019-do-not-repeat-dburl.md) | Do not repeat dburl | Half decided, half overtaken by the code, amended by D125 |
| [D20](D020-postgresql-goes-back-to-9-6-every-other-database.md) | PostgreSQL goes back to 9.6. Every other database starts at the maintained floor | Decided |
| [D21](D021-drop-a-server-version-on-a-rule-not-on-a.md) | Drop a server version on a rule, not on a judgment | Decided |
| [D22](D022-test-every-supported-major-not-a-sample-of-them.md) | Test every supported major, not a sample of them | Superseded by D24 |
| [D23](D023-do-not-build-dbtest-first-let-dbmeta-pull-it.md) | Do not build dbtest first. Let dbmeta pull it into existence | Decided |
| [D24](D024-ci-tests-the-latest-version-only-the-matrix-runs.md) | CI tests the latest version only. The matrix runs locally | Supersedes D22, superseded by D42 |
| [D25](D025-test-on-amd64-only-no-build-tags-and-no-platform.md) | Test on amd64 only. No build tags and no platform gates | Decided |
| [D26](D026-no-database-driver-in-the-dbmeta-module.md) | No database driver in the dbmeta module | Amends D11, amended by D48 |
| [D27](D027-split-the-work-in-two-a-nested-test-module-here.md) | Split the work in two: a nested test module here, a shared harness in dbtest | Decided |
| [D28](D028-root-tests-use-a-fake-driver-replaying-captured.md) | Root tests use a fake driver replaying captured data | Decided |
| [D29](D029-pure-go-only-no-single-package-imports-every.md) | Pure Go only. No single package imports every driver | Amended by D48 |
| [D30](D030-dbtpl-is-not-used-to-generate-dbmeta.md) | dbtpl is not used to generate dbmeta | Amended by D71 |
| [D31](D031-models-register-from-internal-one-file-each.md) | Models register from internal, one file each, gated by build tags | Decided |
| [D32](D032-errors-are-constants-of-a-string-type.md) | Errors are constants of a string type | Decided |
| [D33](D033-results-stream-the-package-does-not-materialize.md) | Results stream. The package does not materialize them | Decided |
| [D34](D034-report-capabilities-and-return-a-typed-error.md) | Report capabilities, and return a typed error when asked anyway | Decided |
| [D35](D035-duckdb-is-out-of-the-initial-testing-set.md) | DuckDB is out of the initial testing set | Superseded by D48 |
| [D36](D036-the-client-drives-dbmeta-decides-nothing-about.md) | The client drives. dbmeta decides nothing about the connection | Decided |
| [D37](D037-a-version-is-a-list-of-numbers-with-a-name-and.md) | A version is a list of numbers with a name, and there can be several | Decided |
| [D38](D038-dbmeta-holds-the-version-query-and-will-run-it.md) | dbmeta holds the version query, and will run it on request | Amended in place |
| [D39](D039-queries-are-listed-described-and-rendered-for.md) | Queries are listed, described, and rendered for the client to run | Decided |
| [D40](D040-three-support-tiers-and-a-trigger-that-can.md) | Three support tiers, and a trigger that can remove a version | Amended by D119 |
| [D41](D041-every-model-ships-its-fixtures-beside-its.md) | Every model ships its fixtures beside its queries | Decided |
| [D42](D042-four-releases-per-push-every-release-nightly.md) | Four releases per push, every release nightly | Supersedes D24, amended by D69 |
| [D43](D043-ask-several-models-before-a-dialect-is-declared.md) | Ask several models before a dialect is declared finished | Decided |
| [D44](D044-a-version-key-names-the-product-a-number-alone.md) | A version key names the product. A number alone never does | Decided |
| [D45](D045-a-query-may-answer-partially-once-and-must-say.md) | A query can answer partially, once, and must say so | Amended by D161 |
| [D46](D046-five-object-kinds-are-missing-and-two-consumers.md) | Five object kinds are missing, and two consumers say which | Decided |
| [D47](D047-dbmeta-supplies-the-data-the-consumer-decides.md) | dbmeta supplies the data. The consumer decides what to show | Amended by D146 |
| [D48](D048-cgo-is-allowed-in-the-test-module-and-nowhere.md) | cgo is allowed in the test module, and nowhere else | Amends D26 and D29, supersedes D35 |
| [D49](D049-one-method-on-the-interface-and-a-not-null-is.md) | One method on the interface, and a NOT NULL is not a constraint row | Amended by D121 |
| [D50](D050-documentation-lives-in-docs-and-the-decision-log.md) | Documentation lives in docs, and the decision log stays one file | Amended by D110 and D111 |
| [D51](D051-there-is-no-alias-for-a-nullable-type.md) | There is no alias for a nullable type | Decided |
| [D52](D052-a-test-driver-is-the-one-usql-uses-or-it-is-the.md) | A test driver is the one usql uses, or it is the wrong driver | Amended by D59 and D154 |
| [D53](D053-one-canonical-expectation-checked-in-that-every.md) | One canonical expectation, checked in, that every database must meet | Decided |
| [D54](D054-sql-server-covers-every-release-that-ships-a.md) | SQL Server covers every release that ships a Linux container | Amended by D63 |
| [D55](D055-the-current-user-moves-here-changing-a-password.md) | The current user moves here. Changing a password does not | Decided |
| [D56](D056-the-password-statement-is-built-here-and-run-by.md) | The password statement is built here and run by the caller | Amends D5, amended by D127 |
| [D57](D057-a-windows-machine-is-how-a-pre-2017-sql-server.md) | A Windows machine is how a pre 2017 SQL Server gets tested, and it is Verified | Decided |
| [D58](D058-one-gitignore-in-the-repository-root.md) | One .gitignore, in the repository root | Decided |
| [D59](D059-oracle-is-tested-with-go-ora-v2-until-v3-tags.md) | Oracle is tested with go-ora v2 until v3 tags its fix | Amends D52, amended by D136 and D157 |
| [D60](D060-the-oracle-model-reads-all-views-and-there-is-no.md) | The Oracle model reads ALL_ views, and there is no DBA_ variant | Decided |
| [D61](D061-every-dialect-is-measured-against-every.md) | Every dialect is measured against every principal the product has | Amended in place |
| [D62](D062-cql-cannot-compute-so-the-cassandra-model.md) | CQL cannot compute, so the Cassandra model computes in Scan | Amended by D93 and D200 |
| [D63](D063-support-says-when-a-release-is-too-old.md) | Support says when a release is too old | Amends D54 |
| [D64](D064-the-verified-tier-is-checked-against-the.md) | The Verified tier is checked against the document | Decided |
| [D65](D065-a-windows-machine-rearms-its-evaluation-before.md) | A Windows machine rearms its evaluation before it expires | Decided |
| [D66](D066-the-order-the-remaining-dialects-are-written-in.md) | The order the remaining dialects are written in | Amended by D67, D77, D88, D91, D94 and D129 |
| [D67](D067-impala-cannot-be-a-dbmeta-model-and-clickhouse.md) | Impala cannot be a dbmeta model, and ClickHouse goes first | Amends D66, amended by D146 |
| [D68](D068-every-container-is-started-by-the-runner-and.md) | Every container is started by the runner and named product-release | Amended by D70 and D124 |
| [D69](D069-the-workflow-builds-its-matrix-from-the-go-list.md) | The workflow builds its matrix from the Go list | Amends D42 |
| [D70](D070-the-runner-is-a-go-command-called-dbrun.md) | The runner is a Go command called dbrun | Amends D68 and D12 |
| [D71](D071-nothing-here-is-generated-the-models-are-written.md) | Nothing here is generated. The models are written | Amends D2, D12 and D30, supersedes D11 and D6a |
| [D72](D072-trino-reads-system-jdbc-and-a-catalog-is-a-real.md) | Trino reads system.jdbc, and a catalog is a real level | Decided |
| [D73](D073-presto-is-its-own-dialect-and-not-a-flavor-of.md) | Presto is its own dialect, and not a flavor of Trino | Decided |
| [D74](D074-firebird-has-no-schemas-and-none-is-invented.md) | Firebird has no schemas, and none is invented | Decided |
| [D75](D075-every-container-is-bounded-and-four-run-at-once.md) | Every container is bounded, and four run at once | Amended by D98 and D108 |
| [D76](D076-sap-hana-reads-sys-and-answers-more-than.md) | SAP HANA reads SYS, and answers more than anything but PostgreSQL | Decided |
| [D77](D077-exasol-will-not-run-here-and-hive-goes-ahead-of.md) | Exasol will not run here, and Hive goes ahead of it | Amends D66, amended by D84 |
| [D78](D078-hive-reads-sys-and-is-a-model.md) | Hive reads sys, and is a model | Decided |
| [D79](D079-a-dialect-that-cannot-bind-renders-its-values.md) | A dialect that cannot bind renders its values | Decided |
| [D80](D080-the-driver-registry-is-dburl-s-and-reading-it-is.md) | The driver registry is dburl's, and reading it is not importing it | Amended by D125 and D154 |
| [D81](D081-the-cassandra-dialect-is-cql.md) | The Cassandra dialect is cql | Decided, amended by D196 |
| [D82](D082-ci-compiles-once-and-every-job-runs-the-binary.md) | CI compiles once and every job runs the binary | Decided |
| [D83](D083-a-server-is-ready-when-it-can-run-a-query-and.md) | A server is ready when it can run a query, and keeps being able to | Decided |
| [D84](D084-exasol-runs-after-all-on-the-nano-image.md) | Exasol runs after all, on the nano image | Amends D77 |
| [D85](D085-exasol-is-the-nano-containers-and-one-frozen.md) | Exasol is the nano containers and one frozen virtual machine | Decided |
| [D86](D086-a-machine-is-one-list-with-a-spec-for-how-it-is.md) | A machine is one list, with a spec for how it is built | Decided |
| [D87](D087-exasol-is-a-model-read-from-the-exa-all-views.md) | Exasol is a model, read from the EXA_ALL views | Decided, amended by D234 |
| [D88](D088-vertica-is-a-model-on-four-community-images.md) | Vertica is a model, on four community images | Amends D66, amended by D100 |
| [D89](D089-agent-skills-are-committed-as-copies-and-one.md) | Agent skills are committed as copies, and one command installs them | Amended by D110 |
| [D90](D090-a-database-qualifies-when-it-is-free-to-run-for.md) | A database qualifies when it is free to run for development and testing | Decided |
| [D91](D091-scylladb-is-a-flavor-of-the-cassandra-model.md) | ScyllaDB is a flavor of the Cassandra model | Amends D66, amended by D92 |
| [D92](D092-a-second-version-statement-reads-the-scylladb.md) | A second version statement reads the ScyllaDB release | Amends D91 |
| [D93](D093-the-cql-tests-use-github-com-xo-cql-which.md) | The cql tests use github.com/xo/cql, which reports a NULL | Amends D62, amended by D196 |
| [D94](D094-couchbase-runs-under-dbrun-and-its-model-waits.md) | Couchbase runs under dbrun, and its model waits for the n1ql rewrite | Amends D66, amended by D95, D96 and D104 |
| [D95](D095-the-couchbase-model-waits-for-the-dbimp-driver.md) | The Couchbase model waits for the dbimp driver | Amends D94, amended by D101 and D104 |
| [D96](D096-couchbase-gets-an-ordinary-user-and-starts-again.md) | Couchbase gets an ordinary user, and starts again after a stop | Amends D94, amended by D104 |
| [D97](D097-dbrun-is-documented-for-its-users-in-dbrun-and.md) | dbrun is documented for its users, in DBRUN and CONTAINERS | Decided |
| [D98](D098-a-server-has-an-owner-and-dbrun-acts-only-on-the.md) | A server has an owner, and dbrun acts only on the caller's own | Amends D75, amended by D102, D108 and D115 |
| [D99](D099-dburl-names-the-product-that-a-scheme-drives.md) | dburl names the product that a scheme drives | Amended by D125 |
| [D100](D100-the-vertica-images-live-in-usql-vertica-and-the.md) | The Vertica images live in usql/vertica, and the older ones wait for admintools | Amends D88 |
| [D101](D101-couchbase-is-the-dialect-couchbase-read-through.md) | Couchbase is the dialect couchbase, read through the dbimp driver | Amends D95 |
| [D102](D102-dbrun-prints-every-principal-of-a-server.md) | dbrun prints every principal of a server | Amends D98 |
| [D103](D103-surrealdb-runs-under-dbrun-for-the-dbimp-driver.md) | SurrealDB runs under dbrun, for the dbimp driver | Amended by D119 |
| [D104](D104-the-couchbase-model-reads-7-6-and-later.md) | The Couchbase model reads 7.6 and later | Amends D94, D95 and D96 |
| [D105](D105-dbrun-runs-a-setup-again-after-a-failure-and.md) | dbrun runs a setup again after a failure, and prints the log of a server that never answered | Amended by D107 |
| [D106](D106-neo4j-enterprise-runs-under-dbrun-under-the.md) | Neo4j Enterprise runs under dbrun, under the evaluation agreement | Amended by D109 and D119 |
| [D107](D107-the-hive-setup-runs-from-a-copy-that-is-safe-to.md) | The Hive setup runs from a copy that is safe to run twice | Amends D105 |
| [D108](D108-eight-servers-run-at-once-and-a-stopped.md) | Eight servers run at once, and a stopped container belongs to nobody | Amends D75 and D98 |
| [D109](D109-neo4j-is-the-dialect-neo4j-and-its-url-names-the.md) | Neo4j is the dialect neo4j, and its URL names the database | Amends D106, amended by D112 |
| [D110](D110-every-xo-repository-is-set-up-for-agents-alike.md) | Every xo repository is set up for coding agents the same way | Amends D50 and D89 |
| [D111](D111-a-large-project-keeps-one-file-per-decision.md) | A large project keeps one file per decision | Amends D50 |
| [D112](D112-eight-servers-run-under-dbrun-for-dbimp.md) | Eight more servers run under dbrun for dbimp's drivers | Amends D109, amended by D114, D119, D123, D134 and D153 |
| [D113](D113-three-avatica-servers-run-under-dbrun.md) | Three Avatica servers run under dbrun for dbimp's driver | Amended by D119 and D155 |
| [D114](D114-a-server-can-answer-more-than-one-dialect.md) | A server can answer more than one dialect | Amends D112, amended by D119 |
| [D115](D115-a-server-shows-its-owners-name-and-status-a-shows.md) | A server shows its owner's name, and status -a shows the stopped ones | Amends D98 |
| [D116](D116-dbrun-knows-the-embedded-databases-before-their-models.md) | dbrun knows the embedded databases before their models | Amended by D119 and D142 |
| [D117](D117-a-hosted-service-appears-in-dbrun-when-its-credential-does.md) | A hosted service appears in dbrun when its credential does | Amended by D125 |
| [D118](D118-every-database-usql-or-dbimp-reaches-gets-an-entry.md) | Every database that usql or dbimp reaches gets an entry | Amended by D123, D141, D145, D215 and D225 |
| [D119](D119-a-release-no-model-reads-is-staged.md) | A release that no model reads is Staged | Amends D40, D103, D106, D112, D113, D114 and D116, amended by D120 and D142 |
| [D120](D120-a-staged-release-keeps-its-cadence.md) | A Staged release keeps its cadence | Amends D119 |
| [D121](D121-the-interface-is-named-queryer.md) | The interface is named Queryer | Amends D49 |
| [D122](D122-dbrun-calls-the-runner-as-few-times-as-it-can.md) | dbrun calls the runner as few times as it can | Decided |
| [D123](D123-cockroachdb-and-cratedb-have-dialects-of-their-own.md) | CockroachDB and CrateDB have dialects of their own | Amends D112 and D118 |
| [D124](D124-a-server-can-publish-a-second-port.md) | A server can publish a second port | Amends D68 |
| [D125](D125-each-wire-compatible-product-has-a-dialect-of-its-own.md) | Each wire compatible product has a dialect of its own | Amends D19, D80, D99 and D117 |
| [D126](D126-a-version-query-waits-for-the-product-s-model.md) | A version query waits for the product's model | Decided |
| [D127](D127-quoting-rules-live-in-dbmeta.md) | Quoting rules live in dbmeta | Amends D56 |
| [D128](D128-cockroachdb-lists-crdb-internal.md) | CockroachDB lists crdb_internal | Decided |
| [D129](D129-an-embedded-product-waits-for-a-model.md) | An embedded product waits for a model | Amends D66, amended by D142 |
| [D130](D130-db2-is-out-of-scope.md) | Db2 is out of scope | Decided |
| [D131](D131-cratedb-answers-no-text-search-kind.md) | CrateDB answers no text search kind | Decided |
| [D132](D132-netezza-is-out-of-scope.md) | Netezza is out of scope | Decided |
| [D133](D133-tidb-shares-the-mysql-model.md) | TiDB shares the mysql model | Decided |
| [D134](D134-tdengine-is-removed.md) | TDengine is removed | Amends D112 |
| [D135](D135-vitess-shares-the-mysql-model.md) | Vitess shares the mysql model and names a schema by its keyspace | Decided |
| [D136](D136-oracle-binds-a-flag-as-a-number.md) | Oracle binds a flag as a number, and is tested on go-ora v3 too | Amends D59, amended by D157 |
| [D137](D137-a-child-kind-takes-parent-for-its-owner.md) | A child kind takes parent for its owner and name for itself | Decided |
| [D138](D138-tables-takes-types.md) | Tables takes types, bound as one string | Decided |
| [D139](D139-a-column-has-a-collation.md) | A column has a collation | Decided |
| [D140](D140-databend-reads-its-system-database.md) | Databend reads its system database | Decided |
| [D141](D141-singlestore-shares-the-mysql-model.md) | SingleStore shares the mysql model, and runs with no license | Amends D118 |
| [D142](D142-ql-is-removed.md) | ql is removed | Amends D116, D119 and D129 |
| [D143](D143-a-dialect-says-how-its-sql-is-written.md) | A dialect says how its SQL is written | Decided, amended by D230 |
| [D144](D144-snowflake-and-redshift-are-written-before-they-run.md) | Snowflake and Redshift are written before they run | Decided, amended by D182 and D190 |
| [D145](D145-impala-runs-in-one-container.md) | Impala runs in one container | Amends D118 |
| [D146](D146-a-query-can-walk-several-statements.md) | A query can walk several statements | Amends D47 and D67, amended by D159 and D175 |
| [D147](D147-the-fields-psql-prints-that-usql-needed.md) | The fields psql prints that usql needed | Decided, amended by D201 |
| [D148](D148-rqlite-shares-the-sqlite3-model.md) | rqlite shares the sqlite3 model | Amended by D151 |
| [D149](D149-oracle-and-singlestore-answer-extended-statistics.md) | Oracle and SingleStore answer extended statistics | Decided |
| [D150](D150-oracle-11g-reads-two-views-slowly.md) | Oracle 11g reads two views slowly | Decided |
| [D151](D151-rqlite-tests-use-dbimps-driver.md) | The rqlite tests use dbimp's driver | Amends D148 |
| [D152](D152-influxdb-3-reads-datafusions-information-schema.md) | InfluxDB 3 reads DataFusion's information_schema | Amended by D170 |
| [D153](D153-libsql-has-an-ordinary-user-through-a-jwt.md) | libSQL has an ordinary user through a JWT | Amends D112, amended by D160 |
| [D154](D154-a-test-driver-is-the-one-dburl-names.md) | A test driver is the one dburl names | Amends D52 and D80, amended by D157, D229, D231 and D234 |
| [D155](D155-the-avatica-servers-speak-json.md) | The Avatica servers speak JSON | Amends D113 |
| [D156](D156-a-test-checks-the-simple-english-rules-a-machine-can.md) | A test checks the simple English rules that a machine can check | Decided |
| [D157](D157-oracle-is-tested-on-go-ora-v3-at-the-fixing-commit.md) | Oracle is tested on go-ora v3 at the commit that fixes it | Amends D59, D136 and D154, amended by D166 |
| [D158](D158-h2-and-voltdb-wait.md) | H2 and VoltDB wait | Decided, amended by D188 |
| [D159](D159-influxql-can-walk-show-statements.md) | InfluxQL can walk SHOW statements | Amends D146, amended by D175 |
| [D160](D160-libsql-shares-the-sqlite3-model.md) | libSQL shares the sqlite3 model | Amends D153, amended by D167 |
| [D162](D162-neo4j-maps-a-database-to-a-schema-and-a-label-to-a.md) | Neo4j maps a database to a schema and a label to a table | Decided |
| [D161](D161-ydb-reads-its-sys-views-and-a-directory-is-a-schema.md) | YDB reads its .sys views, and a directory is a schema | Amends D45 |
| [D163](D163-arangodb-maps-a-collection-onto-a-table.md) | ArangoDB maps a collection onto a table | Amended by D168 |
| [D165](D165-influxql-maps-a-database-to-a-schema-and-a.md) | InfluxQL maps a database to a schema and a measurement to a table | Decided |
| [D164](D164-surrealdb-maps-a-namespace-to-a-catalog-and-a-field-to-a.md) | SurrealDB maps a namespace to a catalog and a defined field to a column | Amended by D168 and D192 |
| [D166](D166-oracle-19c-pinot-and-the-influxdb-release-wait.md) | Oracle 19c, Pinot and the InfluxDB release wait | Amends D157 |
| [D167](D167-the-dsn-is-what-sql-open-takes-and-api-is-the-http-address.md) | The DSN is what sql.Open takes, and api is the HTTP address | Amends D160 |
| [D168](D168-ken-reviews-the-six-dialects-of-2026-10-01.md) | Ken reviews the six dialects of 2026-10-01 | Amends D163 and D164 |
| [D169](D169-each-pattern-and-open-move-from-usql.md) | Each, Pattern and Open move from usql | Decided |
| [D170](D170-influxdb-3-answers-triggers-from-the-processing-engine.md) | InfluxDB 3 answers Triggers from the processing engine | Amends D152 |
| [D171](D171-druid-maps-a-datasource-to-a-table-and-reads-information-schema.md) | Druid maps a datasource to a table and reads INFORMATION_SCHEMA | Decided |
| [D172](D172-the-trino-and-presto-tests-use-dbimps-driver.md) | The Trino and Presto tests use dbimp's driver | Decided |
| [D173](D173-the-odbc-fallback-belongs-to-the-client.md) | The ODBC fallback belongs to the client | Decided |
| [D174](D174-the-clickhouse-tests-use-dbimps-driver.md) | The ClickHouse tests use dbimp's driver | Decided |
| [D175](D175-elasticsearch-and-opensearch-can-walk-show-statements.md) | Elasticsearch and OpenSearch can walk SHOW statements | Amends D146 and D159 |
| [D176](D176-the-first-search-dialects-follow-the-survey-answers.md) | The search dialects follow Ken's answers to the survey | Amended by D183 and D192 |
| [D178](D178-drill-maps-a-workspace-to-a-schema-and-needs-the-metastore-for-file-tables.md) | Drill maps a workspace to a schema and needs the Metastore for file tables | Decided |
| [D177](D177-elasticsearch-maps-an-index-to-a-table-and-walks-sys-and-show.md) | Elasticsearch maps an index to a table and walks SYS and SHOW | Decided, amended by D191 |
| [D179](D179-solr-maps-a-collection-to-a-table-under-a-fixed-schema.md) | Solr maps a collection to a table under a fixed schema | Decided, amended by D191 |
| [D180](D180-voltdb-has-no-catalog-a-statement-can-reach.md) | VoltDB has no catalog that a statement can reach | Decided |
| [D181](D181-opensearch-maps-an-index-to-a-table-and-walks-show-and-describe.md) | OpenSearch maps an index to a table and walks SHOW and DESCRIBE | Decided, amended by D189 and D191 |
| [D182](D182-redshift-ran-against-redshift-serverless.md) | Redshift ran against Redshift Serverless | Amends D144, amended by D204 |
| [D183](D183-aliases-are-tables-and-dbimp-supplies-the-version.md) | Aliases are tables where SQL cannot tell them, and dbimp supplies the version | Amends D176, amended by D191 |
| [D184](D184-dynamodb-has-no-catalog-a-statement-can-reach.md) | DynamoDB has no catalog that a statement can reach | Decided |
| [D185](D185-pinot-has-no-catalog-a-statement-can-reach.md) | Apache Pinot has no catalog that a statement can reach | Decided |
| [D186](D186-avatica-reads-the-hsqldb-catalog-and-phoenix-has-no-model.md) | Avatica reads the HSQLDB catalog, and Phoenix has no model | Decided |
| [D187](D187-gizmosql-shares-the-duckdb-model-and-the-tests-open-the-session.md) | GizmoSQL shares the DuckDB model and the tests open the session | Decided |
| [D188](D188-no-dialect-for-h2-voltdb-chai-or-csvq.md) | No dialect for H2, VoltDB, chai or csvq | Amends D158, amended by D194 |
| [D189](D189-dbimp-reads-the-opensearch-2-19-6-describe-row.md) | dbimp reads the OpenSearch 2.19.6 DESCRIBE row | Amends D181 |
| [D190](D190-snowflake-ran-against-a-trial-account.md) | Snowflake ran against a trial account | Amends D144, amended by D193, D203 and D213 |
| [D191](D191-elasticsearch-solr-and-opensearch-read-the-release-with-select-version.md) | Elasticsearch, Solr and OpenSearch read the release with SELECT version() | Amends D177, D179, D181 and D183, amended by D192 |
| [D192](D192-the-ordinary-user-reads-the-release-and-surrealdb-reads-it-with-select-version.md) | The ordinary user reads the release, and SurrealDB reads it with SELECT version() | Amends D191, D176 and D164 |
| [D193](D193-snowflake-parity-conformance-and-password-were-measured.md) | Snowflake parity, conformance and the password statement were measured | Amends D190, amended by D203 and D213 |
| [D194](D194-no-models-for-hosted-services-beyond-redshift-and-snowflake.md) | No models for hosted services beyond Redshift and Snowflake | Amends D188, amended by D216, D218, D222 and D224 |
| [D195](D195-cassandra-and-scylladb-have-a-url-dsn-and-an-ordinary-user.md) | Cassandra and ScyllaDB have a URL DSN and an ordinary user | Decided, amended by D196 |
| [D196](D196-the-cassandra-dialect-is-cassandra.md) | The Cassandra dialect is cassandra | Amends D81, D93 and D195 |
| [D197](D197-three-fields-can-be-null-in-the-postgresql-catalog.md) | Three fields can be NULL in the PostgreSQL catalog | Decided |
| [D198](D198-describe-fields-for-relations-indexes-columns-and-functions.md) | Describe fields for relations, indexes, columns and functions | Decided |
| [D199](D199-describe-sections-for-partitions-inheritance-policies-and-rules.md) | Describe sections for partitions, inheritance, policies and rules | Decided, amended by D201 |
| [D200](D200-a-binding-can-keep-rows-for-a-product-that-cannot-filter.md) | A binding can keep rows for a product that cannot filter | Amends D62, amended by D202 |
| [D201](D201-the-third-group-of-describe-data-for-usql.md) | The third group of describe data for usql | Amends D147 and D199 |
| [D202](D202-cassandra-matches-names-ignoring-case-and-the-models-share-two-helpers.md) | Cassandra matches names ignoring case, and the models share two helpers | Amends D200 |
| [D203](D203-snowflake-reads-the-columns-of-a-key-through-the-pipe-operator.md) | Snowflake reads the columns of a key through the pipe operator | Amends D190 and D193, amended by D213 |
| [D204](D204-redshift-answers-the-grants-and-the-roles-and-takes-every-password.md) | Redshift answers the grants and the roles, and takes every password | Amends D182 |
| [D205](D205-the-mysql-family-fills-the-new-fields-of-d198-to-d203.md) | The MySQL family fills the new fields of D198 to D203 | Decided, amended by D212 |
| [D206](D206-sql-server-and-oracle-fill-the-describe-fields.md) | SQL Server and Oracle fill the describe fields | Decided, amended by D211 |
| [D207](D207-snowflake-redshift-cratedb-and-questdb-fill-the-describe-fields-they-have-a-source-for.md) | Snowflake, Redshift, CrateDB and QuestDB fill the describe fields they have a source for | Decided, amended by D212 and D232 |
| [D208](D208-clickhouse-duckdb-sqlite3-databend-and-vertica-fill-the-describe-fields-they-have-a-source-for.md) | ClickHouse, DuckDB, SQLite3, Databend and Vertica fill the describe fields they have a source for | Decided |
| [D209](D209-firebird-sap-hana-exasol-hive-and-impala-fill-the-describe-fields-they-have-a-source-for.md) | Firebird, SAP HANA, Exasol, Hive and Impala fill the describe fields they have a source for | Decided, amended by D212 |
| [D210](D210-the-products-without-a-relational-catalog-fill-the-describe-fields-they-have-a-source-for.md) | The products without a relational catalog fill the describe fields they have a source for | Decided, amended by D212 |
| [D211](D211-policy-has-an-enabled-field-that-sql-server-oracle-and-vertica-fill.md) | Policy has an Enabled field that SQL Server, Oracle and Vertica fill | Amends D206 |
| [D212](D212-firebird-and-mariadb-enforce-every-constraint-arangodb-counts-and-redshift-reads-svv-table-info.md) | Firebird and MariaDB enforce every constraint, ArangoDB counts, and Redshift reads SVV_TABLE_INFO | Amends D205, D207, D209 and D210, amended by D221 and D232 |
| [D213](D213-snowflake-tests-and-dbrun-use-the-driver-of-dbimp.md) | Snowflake tests and dbrun use the driver of dbimp | Amends D190, D193 and D203, amended by D214 |
| [D214](D214-the-piped-snowflake-statement-reads-booleans-since-dbimp-v0-16-1.md) | The piped Snowflake statement reads booleans since dbimp v0.16.1 | Amends D213 |
| [D215](D215-spanner-omni-replaces-the-cloud-spanner-emulator.md) | Spanner Omni replaces the Cloud Spanner emulator | Amends D118, amended by D217 |
| [D216](D216-spanner-gets-a-model-that-reads-spanner-omni.md) | Spanner gets a model that reads Spanner Omni | Amends D194, amended by D219 |
| [D217](D217-the-cloud-spanner-emulator-returns-beside-spanner-omni.md) | The Cloud Spanner emulator returns beside Spanner Omni | Amends D215 |
| [D218](D218-the-hosted-services-get-models-and-dbrun-names-their-key-files.md) | The hosted services get models and dbrun names their key files | Amends D194 |
| [D219](D219-spanner-is-measured-on-cloud-spanner-and-a-database-role-is-its-principal.md) | Spanner is measured on Cloud Spanner and a database role is its principal | Amends D216 |
| [D220](D220-bigquery-gets-a-model-that-reads-the-hosted-service.md) | BigQuery gets a model that reads the hosted service | Decided, amended by D226 and D229 |
| [D221](D221-redshift-reads-the-spectrum-external-tables.md) | Redshift reads the Spectrum external tables | Amends D212 |
| [D222](D222-athena-gets-a-model-that-reads-the-hosted-service.md) | Athena gets a model that reads the hosted service | Amends D194, amended by D227 and D229 |
| [D223](D223-no-cosmos-model-because-the-driver-reads-only-documents.md) | No Cosmos DB model, because the driver reads only documents | Decided, amended by D228 |
| [D224](D224-databricks-gets-a-model-that-reads-the-hosted-service.md) | Databricks gets a model that reads the hosted service | Amends D194, amended by D229 |
| [D225](D225-the-bigquery-and-cosmos-entries-follow-the-drivers-of-dbimp.md) | The BigQuery and Cosmos DB entries follow the drivers of dbimp | Amends D118, amended by D228 |
| [D226](D226-bigquery-reads-the-schemas-and-the-grants-on-the-dataset.md) | BigQuery reads the schemas and the grants on the dataset | Amends D220 |
| [D227](D227-the-athena-dialect-is-athena.md) | The Athena dialect is athena | Amends D222 |
| [D228](D228-cosmos-gets-a-model-that-reads-the-hosted-account.md) | Cosmos DB gets a model that reads the hosted account | Amends D223 and D225, amended by D229 and D233 |
| [D229](D229-the-hosted-tests-move-to-the-drivers-of-dbimp-and-spanner-stays-on-go-sql-spanner.md) | The hosted tests move to the drivers of dbimp and Spanner stays on go-sql-spanner | Amends D154, D220, D222, D224 and D228, amended by D231 |
| [D230](D230-the-common-facts-of-the-usql-drivers-move-here.md) | The common facts of the usql drivers move here | Amends D143 |
| [D231](D231-hosted-spanner-tests-use-the-driver-of-dbimp-and-omni-has-its-own-binary.md) | Hosted Spanner tests use the driver of dbimp and Omni has its own binary | Amends D229 and D154 |
| [D232](D232-redshift-reads-table-size-and-rows-only-on-request.md) | Redshift reads table size and rows only on request | Amends D207 and D212 |
| [D233](D233-cosmos-answers-five-more-kinds-from-the-new-dbimp-statements.md) | Cosmos DB answers five more kinds from the new dbimp statements | Amends D228 |
| [D234](D234-the-exasol-tests-use-the-driver-of-dbimp.md) | The Exasol tests use the driver of dbimp | Amends D154 and D87 |
| [D235](D235-the-image-pins-moved-on-2026-10-22.md) | The image pins moved on 2026-10-22 | Decided |
| [D236](D236-dbrun-cli-launches-the-native-client.md) | dbrun cli launches the native client | Decided |

---

### BACKLOG (`docs/BACKLOG.md`)

# Backlog

This document lists work that is known and not done. Each item names the
decision or the measurement that found it.

A decision is not a backlog item. It goes in [`decisions/`](decisions/README.md).
A question for Ken goes at the end of [`PLAN.md`](PLAN.md), under Open questions
for Ken. When an item here is done, delete it, and record in `decisions/`
anything that was decided on the way (D110).

## Servers

### Finish the native clients of dbrun cli

D236 added `dbrun cli`. Three things are left:

1. Ken chose on 2026-10-22 that `cqlsh` and `sqlplus` keep the password as an
   argument, with a note, and that `dbrun` writes no temporary file for them.
   Nine clients take it that way: `sqlplus`, `cqlsh`, `mongosh`, `hdbsql`,
   `arangosh`, `rqlite`, `cbq`, `ksql` and `exaplus`. The last seven need a
   measurement of what each client offers.
2. Only `psql`, `mysql` and `mariadb` were run, and `psql` on Redshift. Run the
   other clients against a server and correct the forms that are wrong.
3. Redshift, Neon and PlanetScale have a client. The other hosted services have
   none, and each one says why in `hosted/hosted.go`. A hosted service has one
   credential, so `--as` is not supported. Running a client inside the
   container when the host has none is open. Ken chose host only for now.

### Decide the pins that need Ken

D235 moved most pins and left four for Ken. The first is Solr 9.11.0, a new line
that was rebuilt on 2026-10-17, and the rule in `container/solr.go` does not say
whether it replaces 9.10.1 or sits beside it. The second is SingleStore image
0.2.86, which needs `dbrun` to measure its engine before the pin moves. The third
is the MariaDB 10.6 floor, because the vendor ended it in 2026-07 and the image
is still patched. The fourth is the Cassandra 3.11 floor, which was not rebuilt
since 2025-11-14.

ClickHouse 25.3 is the next pin to look at, because it was not rebuilt for eight
and a half months. The comment on SQL Server 2017 in `container/sqlserver.go` is
also stale. It says the image is built on Ubuntu 16.04, and the image now reports
Ubuntu 18.04.

### Find why the Hive setup fails on a slow machine

The Hive setup fails on a GitHub runner, soon after HiveServer2 first answers,
with a SerDeException: "proto.class has to be set". It comes from one of the
`PROTO_` tables at the end of the schema script, which dbmeta does not read.
It never failed on a development machine. D105 runs the setup again after a
failure, and D107 made the script safe to run twice, so a retry can get past
it. The cause is not known.

### Read the log when Oracle 21c does not answer

`oracle-21c` did not answer in five minutes on one CI run, and it answers in
under a minute on every other run. The cause was not recorded. D105 now
prints the last 20 lines of the container's log when a server never answers.
When it happens again, read that log and decide whether the entry needs a
change.

### Find why oracle-26ai and QuestDB failed in CI

On 2026-10-08 the first CI run on 90d0261 failed two jobs, and both passed when
rerun, so neither touches that change. `oracle-26ai` failed in the image's
setup, when it reset the SYSTEM password, with ORA-65048 on `ALTER USER SYSTEM`.
It failed again on 76f266b the same day, at the same statement, with ORA-04021,
a timeout while it waited to lock an object. So the setup of the 26ai image
races a job that the database starts itself, and two runs in about thirty
lost. `questdb-9.4.3` failed once, in `TestQuestDBFixtureObjects` at
`questdb_test.go:216`, with the view `recent` listed and the view `book`
missing. The cause of neither is known. For Oracle, read what the image runs
at start and whether `dbrun` can wait for it, or start the server again when
that statement fails.

`questdb-9.4.3` and `questdb-10.0.1` each failed once on 2026-10-10, in
`TestPrivilegeParity` for the read-only principal: the `tables` query read "the
same rows with different values". The cause was `Table.Rows` (D207), which reads
`table_row_count`: a WAL table applies its writes later, so the count differed
between two reads, and the setup waited only for the table `book`. The setup now
waits until `wal_tables()` shows no table with `sequencerTxn` above `writerTxn`.
If the failure returns, read the log first.

### Find why Databend's first start can fail

The first start of `databend-1.2.948` after the pull of the image did
not answer. The image's bootstrap script starts the query server one second
after the metadata server, and the metadata server was still waiting to become
the leader, so the query server gave up with "cannot connect to
http://0.0.0.0:9191". It did not happen again in four fresh starts (D112). If
it comes back in CI, the entry can start the two servers itself and wait for
the metadata server between them.

### Move the ceilings that move

Three ceilings move faster than the others. Databend publishes a weekly
release almost every day, ArangoDB's release is the newest 3.12 patch, and
Neo4j's is the newest monthly release (D106, D112). When a newer release is
published, replace it in the file in `container/` and in the row in
`EVALUATION.md`.

### Consider a SQL layer for what a product does not answer

This is not a priority (D166). Apache Pinot keeps its catalog only in the
Controller's REST API, and its Broker's SQL has no information_schema, no
SHOW and no DESCRIBE, measured on 1.4 and 1.5 on 2026-10-01. InfluxDB names
its release only in `GET /ping`. A generic SQL layer, in a driver or beside
one, can answer such statements, for example SHOW TABLES, DESCRIBE and
`SELECT version()`, from what the product does expose. If Ken takes it up,
a Pinot model can walk those statements, as Impala's does (D146), and the
InfluxDB models can read the release with a statement.

### Finish what D204 left open for Redshift

A grant of a table to a role is not in `Privileges`, because only
SVV_RELATION_PRIVILEGES lists it and that view shows a user only the rows of
that user. The routine
parameters, the types, the casts, the aggregates and the operators have a
source in pg_catalog and are not read. The partitions of a Spectrum table
(SVV_EXTERNAL_PARTITIONS) are not read, because the Glue table of the fixture
has none. A run with a partitioned Glue table finishes them (D221).

### Read what Snowflake answers and no kind holds

Measured by D203 and left out. `Roles` is answerable by SHOW ROLES, and Role has
bools that Snowflake has no source for. `Indexes` and `IndexColumns` are
answerable by INFORMATION_SCHEMA.INDEXES and INDEX_COLUMNS for a hybrid table,
which a trial account refuses to make, so no row was read. The same account
refuses masking policies, row access policies and materialized views. A run on
an Enterprise account with a hybrid table and a row access policy finishes
them.

### Settle the size and the rows of CrateDB and QuestDB

D207 measured it. The sources for the size and the row count of a CrateDB table
(`sys.shards`, and `pg_class.reltuples`) and of a QuestDB table
(`table_storage()`) cost a scan of the whole catalog for each read.
`Table.Size` and `Table.Rows` are left NULL for CrateDB, and `Table.Size` for
QuestDB. If either product makes the join follow the filter, the fields can be
added. Redshift reads them only on request (D212, D232).

### Read the options of a Redshift table without SVV_TABLE_INFO

D232 left `Table.Options` NULL for a Redshift table and a view by default,
because the distribution style and the sort key came from SVV_TABLE_INFO. The
columns `reldiststyle` of `pg_class` and `attisdistkey` and `attsortkeyord` of
`pg_attribute` hold the same facts, and every user can read them. They were not
measured. If they answer in one statement with a bounded plan, the options can
be read with no grant, and only the size and the rows need `WithTableInfo`.

### Read the settings of a CrateDB user

`sys.users.session_settings` is the analogue of `RoleSettings`. One statement
returns the object as JSON text and not as `name=value` lines, because CrateDB
cannot read the value of a key that the statement does not name. A later
release can change that.

## PostgreSQL model

### Read the sections of `\d` that no kind answers

D201 left four things out. psql's Referenced by section lists the constraints of
other tables that name this one, and `Constraints` filters by the table that owns
the constraint, so a kind or a filter on `confrelid` is needed. The token types
that `\dFp+` prints come from `ts_token_type` of the parser. The index footer of
`\d NAME` prints the tablespace of each index. `Subscriptions` lists the
subscriptions of every database, and psql narrows to the current one.

### Make the owner of a sequence cost what the rows cost

The `owned_by` column of `Sequences` reads `pg_depend` by `objid` alone, and the
index of that catalog starts with `classid`. Each sequence scans the table, so
5005 sequences took 5.9 seconds on release 15, and one sequence by name takes
0.2 ms. D201 measured it. The fix is to add the class of the sequence and of the
table to the subselect. Measure it before and after, on a catalog of thousands
of sequences.

## Consumers

### Move the rules of the products with no model out of usql

ODBC, csvq and Cosmos keep rules of their own in usql's drivers, such as
ODBC's usql_trim, csvq's SHOW routing and the trailing semicolon Cosmos strips,
and have no model here, so D143's Info fields do not reach them. Athena has a
model now (D222), and its usql driver keeps the same rule for the semicolon.
D230 moved the placeholder, the version text and the query and scan flags.
What is left is these rules, the InfluxDB and csvq versions, and the semicolon.

### usql reads metadata through dbmeta

usql's W21 moved its describe commands, its version and \password onto
dbmeta, and its W31 wires up every psql describe command in
[`COMMANDS.md`](COMMANDS.md). Both are staged in usql and not committed.
When W31 lands, update the column "usql today" in COMMANDS.md, and what
[`USQL.md`](USQL.md) says usql answers.

### Move usql's \copy statements here

D230 moved the placeholder of \copy here, as `Dialect.Placeholder`. The probe of
the columns of a table, `SELECT * FROM t WHERE 1=0`, is still in usql's drivers.
It is not a read of the catalog, so it needs a decision beside D56 before any
of it lands here.

### dbtpl reads metadata through dbmeta

[`DBTPL.md`](DBTPL.md) holds which of the nine reads `dbtpl` needs each model
answers, and whether `dbtpl` can generate from each database. `dbtpl` does
not read `dbmeta` yet.

## Models

### Read the size and the clustered flag of an index

D205 left `Index.Size` and `Index.Clustered` NULL on MariaDB and MySQL. The
sources are `mysql.innodb_index_stats` (the size in pages, a table of the mysql
schema) and `INNODB_INDEXES` or `INNODB_SYS_INDEXES` (the type, where 1 is the
clustered index). Both refuse a user without the PROCESS privilege or SELECT on
the mysql schema, as measured on MariaDB 12.3. They can come back as a
fragment that a parity scene marks as refused for the grantee, if Ken wants the
fields more than the statement that answers for every principal.

### MariaDB puts the compression comment in the data type

`Column.DataType` is `COLUMN_TYPE`, and MariaDB writes `/*M!100301 COMPRESSED*/`
into it for a compressed column, so the type reads `text /*M!100301 COMPRESSED*/`
on 12.3. D205 reads `Column.Compression` from that comment and did not change
`DataType`. A caller that compares types sees the comment.

### Vitess reads the size of one tablet

`Table.Size` and `Rows` on Vitess are the numbers of the tablet that vtgate
picks, and a keyspace with several shards holds the rest elsewhere. A sum over
the shards needs vtgate to run one statement on every shard, which it does not
do for `information_schema`.

### A Validated field on Constraint

D206 fills `Constraint.Enforced` for SQL Server and Oracle as "the server checks
a new row". Both products also say whether the rows that were already there are
known to satisfy the constraint: `is_not_trusted` on SQL Server and `VALIDATED`
on Oracle. PostgreSQL has `convalidated`. No field carries it on `Constraint`.
Add `Validated sql.Null[bool]` to the root type and fill it in the three models.
`NotNull.Validated` already exists.

### Oracle sizes of partitioned tables and subpartitions

`PartitionedTable.DirectSize` and `TotalSize` are NULL on Oracle, and
`Partitions` has no rows for subpartitions (D206). A size needs the connected
user and a sum over `USER_SEGMENTS`. A subpartition needs a kind that is not a
LONG in a UNION.

### Settle the size and the rows of a Vertica table and the encoding of its columns

D208 measured it. The size and the rows of a table are in
`v_monitor.projection_storage` and the encoding of a column is in
`v_catalog.projection_columns`. A subquery on either for every table or column
made a read of one table 3 to 22 times slower, and the cost grew with the
catalog. `Table.Size`, `Table.Rows` and `Column.Compression` are NULL. A
projection of a replicated table is also repeated on every node, so a sum of
rows is not the rows. If a later release gives a view that follows the filter,
the fields can be added.

### Options and size of a SQLite table

D208 leaves `Table.Options` NULL on SQLite. `pragma_table_list` has the strict
and the WITHOUT ROWID flags, and joining it for every table grows with the
square of the catalog. A shadow table is listed as a `table`, and
`pragma_table_list` calls it `shadow`. Ken decides whether the type changes.
`Table.Size` needs `dbstat`, which scans the file and is not in the mattn
driver.

### Row security for Vertica, ClickHouse and Databend

`Table.RowSecurity` is NULL for all three. The Vertica and ClickHouse sources
(`access_policy` and `row_policies`) are refused to or filtered for a user who
is not an administrator, and a flag that reads `false` for that user is a wrong
answer. Databend 1.2.951 has no catalog for its row access policies. A later
release can add one.

### The SETTINGS clause of a ClickHouse table

The settings of a MergeTree table, such as `index_granularity`, are in
`engine_full` as text after SETTINGS. A statement cannot split them, so
`Table.Options` has the clauses that have a column of their own.

### SAP HANA size and row count

`Table.Size`, `Table.Rows` and `Index.Size` are NULL on SAP HANA (D209). The
sources are `SYS.M_TABLES`, `SYS.M_RS_INDEXES` and `SYS.M_CS_INDEXES`, and any
user reads them for the objects it can see. The planner reads one row when the
filter is a literal, a plain `= ?` or a plain `LIKE ?`, and reads every table
when the filter is the `(? = '' OR x LIKE ?)` form that all the models use. Fill
the three fields when one statement can pass a pattern that the planner pushes
down, and measure with 3000 tables.

### Exasol left join to the object sizes

`Table.Size` on Exasol joins `EXA_ALL_OBJECT_SIZES`, which costs 85 ms at 3000
tables for one table, and 41 ms with the fixture only (D209). An inner join
costs 10 ms and can drop a table that a user sees in `EXA_ALL_TABLES` and not
in `EXA_ALL_OBJECT_SIZES`. Run the parity targets with an inner join, and keep
it if the rows do not change.

### Spanner table and index size

`Table.Size`, `Index.Size` and `Tablespace.Size` are NULL on Spanner (D216,
D219). The source is `SPANNER_SYS.TABLE_SIZES_STATS_1HOUR`. On 2026-10-10 a
table with 2000 rows of 500 bytes and an index, in a named schema and in the
default schema, was in Cloud Spanner for 35 minutes and the view held only the
row unknown_table_name with 0 bytes for the two hourly intervals after it.
Cloud Spanner needs more time or more data, and the cause is not known. Check
again with a table that is older than a few hours, and read how the view names a
table in a named schema, which this measurement did not show.

### Spanner objects that no kind holds

Spanner has objects that the 65 kinds have no place for (D216). A property graph is
in `INFORMATION_SCHEMA.PROPERTY_GRAPHS` as one JSON value, a model is in `MODELS`
and refused by Spanner Omni, a placement is in `PLACEMENTS` and needs an instance
partition, a table synonym has no kind but is a `Table` of type synonym, and a
proto bundle is one binary descriptor in `SCHEMATA`. Ask Ken whether any of
them is worth a kind, with the cost test of D47. The ON UPDATE expression, the
hidden flag and the identity kind of a column, the options of a sequence, and
the security type of a view are columns of INFORMATION_SCHEMA that no field holds
either.

### Spanner index definition

`Index.Definition` and `Index.Using` are NULL on Spanner (D216). INFORMATION_SCHEMA
keeps the parts of an index and not its statement. A statement can build
CREATE INDEX from `INDEX_COLUMNS`, `INDEXES` and `INDEX_OPTIONS` with one
aggregate, and the work is to write it and to measure it on a large catalog.

### BigQuery grants on a table and a dataset with no table

`Privileges` reads the IAM bindings of the dataset only, because OBJECT_PRIVILEGES
answers one object at a time, and a table has a row only if somebody sets a binding on
it (D226). Set one on a fixture table and measure the rows. Schemas, CurrentSchema and
Privileges read no row for a dataset with no table or view, because `@@dataset_id` is
NULL and the tables of the dataset name it. A dbimp driver that tells the dataset of the
connection to a statement removes the limit.

### BigQuery on a large catalog

No catalog of thousands of tables was built on BigQuery, because each table is a
job of a second or more and each INFORMATION_SCHEMA read bills 10 MB (D220). Build
one in a loop, run a dry run of each statement, and read the time of the job, to
check the cost test of D47.

### BigQuery generated columns and row access policies

BigQuery refused every generated column that the fixture tried, with "Unsupported
generated column expression", so the mapping of `Column.Generated` is not
measured. A row access policy exists, and no view lists it: the views that DeepSeek and
Gemini named answer not found (D226). See D220.

### Run the Exasol Community Edition machine on the driver of dbimp

D234 moved the Exasol tests to the driver of dbimp, which dbimp tagged in v0.20.0. The
Verified Community Edition machine `exasol-2025.2.1` was not run on it, and the two
`exasol@2025` sections of `parity.txt` were edited by hand. Run `dbrun test exasol-2025.2.1` on a
development machine and read the diff of the goldens.

### Cosmos DB, one index for each entry of the policy

`Indexes` for Cosmos DB is one row for each container, the policy (D233). One row for each
entry, with a composite index as one row, needs a hook that expands a row into several
objects, or a way to drop the rows of a composite index after the first. Both change the
root package, which is Ken's decision. `IndexColumn` has no field for the kind of an entry,
so `Expression` carries it. A `Kind` field on the index column is cleaner. The status
and the parents of a partition key range have no field on `Partition` either.

### Measure what the Cosmos DB account cannot build yet

The account has no vector search, no change feed of all versions, no analytical store and
no region to write to but one, so a vector embedding policy, a vector index, a change feed
retention, an analytical time to live and a custom conflict resolution policy were never
measured, and the autoscale columns of `$offers` were not either. The statements read them
by the names that Microsoft documents, and they are NULL on this account. If an account
gets one of those capabilities, extend the fixture and the tests of Tables and of the index
columns. The vector rows of `$index_paths` were not read, for the same reason.

### Databricks table size, defaults and checks have no source

Databricks answers 17 of the 65 kinds (D224). The size, the row count, the location and
the clustering columns of a table are in DESCRIBE DETAIL. A column default, an identity
column, a generated column and a CHECK constraint are in SHOW CREATE TABLE and SHOW
TBLPROPERTIES. All of them are statements that a SELECT cannot read, and INFORMATION_SCHEMA
reports a default as NULL and an identity column as NO. D146 allows a walk for four
products, and Databricks is not one. A walk costs one statement for each table, and the
warehouse is shared and has a daily quota. Ken decided on 2026-10-11 that Athena gets no
walk, and Databricks follows the same rule. Ask Databricks and dbimp for a relation that holds them: `system.information_schema.tables`
has no size column on the release that was measured.

### Databricks on other principals, a catalog of thousands of tables and a cluster

The reader of the test account sees no routine and no volume, because it holds no
EXECUTE and no READ VOLUME. A principal that holds them was not measured. No catalog of
thousands of tables was built, because the free edition has a daily quota of compute, so
the plan of each statement against a large catalog was not checked. The version of a
cluster, `dbr_version`, was not read, because the account has a warehouse only. The
connection, share, external location and storage credential relations were read on an
account that holds none, so `ForeignServers` was shown to run and the others were left
out (D224).

### Athena table comments, partitions and functions have no source

Athena answers 8 of the 65 kinds (D222). The comment and the location of a table,
the values of a partition and the function list are behind SHOW CREATE TABLE, SHOW
TBLPROPERTIES, SHOW PARTITIONS, DESCRIBE and SHOW FUNCTIONS, which a SELECT cannot
read. D146 allows a walk for four products, and Athena is not one. A walk costs
one statement for each table, and each Athena statement is a job of one to six
seconds. Ken decided on 2026-10-11 that Athena gets no walk. The hidden tables `"t$partitions"`,
`"t$properties"` and `"t$files"` are per table too.

### Athena on a large catalog and on other workgroups

No catalog of thousands of tables was built on Athena, because a table is a job of
several seconds. INFORMATION_SCHEMA scans no data, so the cost of a read is time
and not money. The engine version of the workgroup is not read. A test of the other
principals that an account can hold, such as a role with Lake Formation grants that
hide a table, was not made (D222).

---

### COMMANDS (`docs/COMMANDS.md`)

# psql Commands and the Go API

This maps every `psql` metadata command to the `dbmeta` Go value that answers
it. It exists so that wiring `usql` up later is a lookup rather than a reading
exercise.

The mapping was taken from `exec_command_d` in `src/bin/psql/command.c`, which
is the dispatcher `psql` itself uses, so the command spellings and the grouping
are `psql`'s rather than a reconstruction.

## How to read it

Each row gives a command, the Go value that answers it, and the type that value
yields. Every value is a `*dbmeta.Query[T]`, so the call is the same shape
whichever row you are on:

```go
for v, err := range dbmeta.Tables.All(ctx, m, db, args) {
	// v is a dbmeta.Table
}
```

A caller that wants the statement rather than the rows calls `Build` on the same
value, and `Fields`, `Params` and `Support` describe it. See the package
documentation.

`usql today` says whether `usql` currently implements that command at all. It
implements eleven of them, so most of this table is capability that exists in
`dbmeta` and has no consumer yet.

## Relations

| Command | Go value | Yields | usql today |
| --- | --- | --- | --- |
| `\d` | `dbmeta.Tables` | `Table` | yes |
| `\dt` | `dbmeta.Tables` | `Table` | yes |
| `\dv` | `dbmeta.Tables` | `Table` | yes |
| `\dm` | `dbmeta.Tables` | `Table` | yes |
| `\ds` | `dbmeta.Sequences` | `Sequence` | yes |
| `\di` | `dbmeta.Indexes` | `Index` | yes |
| `\dE` | `dbmeta.ForeignTables` | `ForeignTable` | no |
| `\dP` | `dbmeta.PartitionedTables` | `PartitionedTable` | no |
| `\dn` | `dbmeta.Schemas` | `Schema` | yes |
| `\l` | `dbmeta.Databases` | `Database` | yes |

`\dt`, `\dv` and `\dm` are one query in `psql` and one here. Narrow with the
`types` argument rather than with a different value: a table, a view and a
materialized view differ by `Table.Type`.

## Describing one relation

`\d NAME` is not one query. `psql` assembles it from several, in
`describeOneTableDetails`, which is the largest entry point in `describe.c` and
carries 21 of its 68 version gates. `dbmeta` keeps them separate, because a
caller usually wants one of them.

To reproduce `\d NAME`, read these with the same `Args{Schema, Parent}`:

| Part of the output | Go value | Yields |
| --- | --- | --- |
| the column list | `dbmeta.Columns` | `Column` |
| the index footer | `dbmeta.Indexes` | `Index` |
| the columns of each index | `dbmeta.IndexColumns` | `IndexColumn` |
| the constraint footers | `dbmeta.Constraints` | `Constraint` |
| the trigger footer | `dbmeta.Triggers` | `Trigger` |
| the sequence detail, for a sequence | `dbmeta.Sequences` | `Sequence` |
| the partition detail | `dbmeta.PartitionedTables` | `PartitionedTable` |

The sections that `\d+ NAME` adds are read the same way. Each takes the
parent filter, so one table costs one small statement. D199 holds the types
and the cost.

| Part of the output | Go value | Yields |
| --- | --- | --- |
| Partition key | `dbmeta.PartitionedTables`, the `Expression` field | `PartitionedTable` |
| Partitions, and Partition of with its bound and constraint | `dbmeta.Partitions` | `Partition` |
| Inherits and Child tables | `dbmeta.Inherits` | `Inherit` |
| Policies | `dbmeta.Policies` | `Policy` |
| Row security on and forced | the fields `RowSecurity` and `RowSecurityForced` of `dbmeta.Tables` | `Table` |
| Publications | `dbmeta.PublicationTables` with `schema` and `parent` | `PublicationTable` |
| Statistics objects | `dbmeta.ExtendedStats` with `parent` | `ExtendedStat` |
| Rules | `dbmeta.Rules` | `Rule` |
| Options of a table, a view or an index | the field `Options` of `dbmeta.Tables` and `dbmeta.Indexes` | `Table`, `Index` |
| View definition | `dbmeta.Views` | `View` |
| Server and FDW options of a foreign table | `dbmeta.ForeignTables` | `ForeignTable` |
| A composite type | `dbmeta.Tables` with `types=composite type`, and `dbmeta.Columns` | `Table`, `Column` |
| Not-null constraints, from release 18 | `dbmeta.NotNulls` | `NotNull` |
| the INCLUDE columns of an index | the field `Include` of `dbmeta.IndexColumns` | `IndexColumn` |
| the index text of the footer, such as `btree (label COLLATE "C" text_pattern_ops)` | the field `Using` of `dbmeta.Indexes`. `Definition` is the whole statement | `Index` |
| the constraint kind of an index, and the text of an exclusion constraint | the fields `ConstraintType`, `ConstraintDefinition` and `ConstraintPeriod` of `dbmeta.Indexes` | `Index` |
| the Cache of a sequence | the field `CacheSize` of `dbmeta.Sequences` | `Sequence` |
| the names that psql prints with no schema | the `Visible` fields of `Partition`, `Inherit`, `PartitionedTable`, `ExtendedStat` and `Index`. D201 lists them | |

## Routines and types

| Command | Go value | Yields | usql today |
| --- | --- | --- | --- |
| `\df` | `dbmeta.Functions` | `Function` | yes |
| `\dfa` | `dbmeta.Aggregates` | `Function` | no |
| `\da` | `dbmeta.Aggregates` | `Function` | yes |
| `\dT` | `dbmeta.Types` | `Type` | no |
| `\dD` | `dbmeta.Domains` | `Domain` | no |
| `\do` | `dbmeta.Operators` | `Operator` | no |
| `\dC` | `dbmeta.Casts` | `Cast` | no |
| `\dL` | `dbmeta.Languages` | `Language` | no |

`\dfn`, `\dfp`, `\dft` and `\dfw` narrow `\df` to normal functions, procedures,
triggers and window functions. They are the same query narrowed by
`Function.Kind`, not separate values.

## Users and privileges

| Command | Go value | Yields | usql today |
| --- | --- | --- | --- |
| `\du` | `dbmeta.Roles` | `Role` | no |
| `\dg` | `dbmeta.Roles` | `Role` | no |
| `\drds` | `dbmeta.RoleSettings` | `RoleSetting` | no |
| `\drg` | `dbmeta.RoleGrants` | `RoleGrant` | no |
| `\dp` | `dbmeta.Privileges` | `Privilege` | yes |
| `\z` | `dbmeta.Privileges` | `Privilege` | no |
| `\ddp` | `dbmeta.DefaultACLs` | `DefaultACL` | no |

`\z` and `\dp` are the same command in `psql`.

The two parts of `\dp` that `psql` prints as text are rows too. D201 holds the
reasons.

| Part of the output | Go value | Yields |
| --- | --- | --- |
| Column privileges, one line for each entry | `dbmeta.ColumnPrivileges` with `Args{Schema, Parent}` | `ColumnPrivilege` |
| Policies, with the command, the roles, the expressions and whether the policy is enabled | `dbmeta.Policies` with `Args{Schema, Parent}` | `Policy` |

## Storage and the server

| Command | Go value | Yields | usql today |
| --- | --- | --- | --- |
| `\db` | `dbmeta.Tablespaces` | `Tablespace` | no |
| `\dA` | `dbmeta.AccessMethods` | `AccessMethod` | no |
| `\dO` | `dbmeta.Collations` | `Collation` | no |
| `\dc` | `dbmeta.Conversions` | `Conversion` | no |
| `\dl` | `dbmeta.LargeObjects` | `LargeObject` | no |
| `\dconfig` | `dbmeta.Settings` | `Setting` | no |
| `\dd` | `dbmeta.Comments` | `Comment` | no |
| `\dy` | `dbmeta.EventTriggers` | `EventTrigger` | no |
| `\dX` | `dbmeta.ExtendedStats` | `ExtendedStat` | no |

## Foreign data

| Command | Go value | Yields | usql today |
| --- | --- | --- | --- |
| `\dew` | `dbmeta.ForeignDataWrappers` | `ForeignDataWrapper` | no |
| `\des` | `dbmeta.ForeignServers` | `ForeignServer` | no |
| `\deu` | `dbmeta.UserMappings` | `UserMapping` | no |
| `\det` | `dbmeta.ForeignTables` | `ForeignTable` | no |
| the options of `\dew+`, `\des+`, `\deu+` and `\det+`, and of a foreign table in `\d+` | `dbmeta.ForeignOptions` | `ForeignOption` | no |

`ForeignOption.Quoted` is the option as psql prints it inside the parentheses.
`Options` of each kind is the catalog text and keeps its meaning.

## Replication

| Command | Go value | Yields | usql today |
| --- | --- | --- | --- |
| `\dRp` | `dbmeta.Publications` | `Publication` | no |
| `\dRp+` | `dbmeta.PublicationTables` | `PublicationTable` | no |
| `\dRs` | `dbmeta.Subscriptions` | `Subscription` | no |
| `\dRs+`, the Conninfo column | `dbmeta.SubscriptionConnections` | `SubscriptionConnection` | no |

The other columns of `\dRs+` are fields of `Subscription`. Only a superuser can
read the connection string, so it is a kind of its own. D201 says why.

## Text search

| Command | Go value | Yields | usql today |
| --- | --- | --- | --- |
| `\dF` | `dbmeta.TextSearchConfigs` | `TextSearchConfig` | no |
| `\dF+` | `dbmeta.TextSearchConfigMaps` | `TextSearchConfigMap` | no |
| `\dFp` | `dbmeta.TextSearchParsers` | `TextSearchParser` | no |
| `\dFp+`, the Method and Function rows | `dbmeta.TextSearchParserFunctions` | `TextSearchParserFunction` | no |
| `\dFd` | `dbmeta.TextSearchDictionaries` | `TextSearchDictionary` | no |
| `\dFt` | `dbmeta.TextSearchTemplates` | `TextSearchTemplate` | no |

## Operator families

| Command | Go value | Yields | usql today |
| --- | --- | --- | --- |
| `\dAc` | `dbmeta.OperatorClasses` | `OperatorClass` | no |
| `\dAf` | `dbmeta.OperatorFamilies` | `OperatorFamily` | no |
| `\dAo` | `dbmeta.OperatorFamilyOperators` | `OperatorFamilyOperator` | no |
| `\dAp` | `dbmeta.OperatorFamilyFunctions` | `OperatorFamilyFunction` | no |

## Extensions

| Command | Go value | Yields | usql today |
| --- | --- | --- | --- |
| `\dx` | `dbmeta.Extensions` | `Extension` | no |
| `\dx+` | `dbmeta.ExtensionObjects` | `ExtensionObject` | no |

## What has no mapping

Two gaps, in opposite directions. One is now closed.

`\ss` is a `usql` command with no `psql` equivalent. It shows column statistics
values, and `psql` has never had such a command: no `"ss"` has ever appeared in
`command.c` and `describe.c` has never read `pg_stats`. Jan Was added it to
`usql` in 2021, implementing it for PostgreSQL from `pg_stats` and for Trino
from `SHOW STATS FOR`, and DuckDB gained it later with the same Trino syntax.

`dbmeta.ColumnStats` now answers it. The decision it needed was which shape to
take, because the databases disagree: PostgreSQL reports `null_frac`,
`n_distinct` and `most_common_vals`, Trino and DuckDB return the result of
`SHOW STATS FOR`, MariaDB keeps `mysql.column_stats` with its own columns, and
MySQL 8 keeps a JSON histogram under a view named like MariaDB's and holding
something else. D9 does not settle it, because PostgreSQL's shape is not
obviously right for a histogram.

The shape taken is PostgreSQL's, with every field nullable. A database that
computes something reports it and a database that does not reports absent,
which is the padding rule applied to a kind rather than to a release.
PostgreSQL, MariaDB, SQL Server, Oracle, SAP HANA, Apache Hive, CrateDB,
Databend, SingleStore and Impala answer. MySQL, SQLite and the other models
report `ErrNotSupported`, because none of them has a width, a null fraction or
a distinct count to give. A row of absences is worse than no row. See
COVERAGE.md.

`\sf` and `\sv` show the source of a function or a view. They live outside
`describe.c` and are not part of the 49, so they are not in this table.
`Function.Definition` answers the first, and `dbmeta.Views` answers the
second. `Function.Source` holds the body where a product keeps one apart from
the statement (D147).

## The kinds with no psql command

Seven object kinds here answer no `psql` command. They exist because `usql` and
`dbtpl` were measured and needed them, and D47 allows them: `psql` sets the
object model and does not set the column set.

| Go value | Yields | Why psql has no command |
| --- | --- | --- |
| `dbmeta.ConstraintColumns` | `ConstraintColumn` | `psql` prints a constraint as one line of text, which `Constraint.Definition` still holds |
| `dbmeta.RoutineParameters` | `RoutineParameter` | `psql` prints a signature as one line, which `Function.ArgTypes` still holds |
| `dbmeta.EnumValues` | `EnumValue` | `\dT+` prints the labels joined into one string, which `Type.Elements` still holds |
| `dbmeta.Views` | `View` | `\d name` on a view prints the definition, and `\sv` shows it outside `describe.c` |
| `dbmeta.ColumnStats` | `ColumnStat` | `psql` has no such command. `usql` added `\ss` |
| `dbmeta.CurrentSchema` | `Schema` | session state rather than an object. `dbtpl` reads it in every loader |
| `dbmeta.CurrentUser` | `User` | session state rather than an object. It came from auditing `usql` (D55) |

In each of the first three the prose and the parts are both available. The
parts are authoritative and the prose is what `psql` prints, so a client
matching `psql` output does not have to rebuild the string for every dialect.

Two fields follow the same rule. `Column.PrimaryKey` is in the row MySQL and
SQLite already select and costs PostgreSQL one join. `Function.ID` identifies a
routine where the name does not, and `RoutineParameters` carries the same value
so that the join is one expression everywhere.

## Wiring usql up

An agent doing that work needs three things beyond this table.

The caller supplies the dialect and version. `usql` already reads a version
string per driver, and `dbmeta.Dialect.Version` replaces that. The dialect is
`dburl.URL.Dialect`, which dburl sets when it parses a URL, from v0.32.0.

Read `URL.Dialect` and never `URL.Driver`. `dburl` registers a scheme per Go
driver, so `URL.Driver` names the driver: `pgx` or `moderncsqlite`,
and from dburl's D22 `pgx` for `postgres://` too. None of those is a dialect
here. `URL.Dialect` is `postgres` or `sqlite3` for each of them,
because dburl holds that taxonomy and hard rule 1 keeps it out of `dbmeta`.
D99 in [`decisions/`](decisions/README.md) has it.

The flavor needs nothing. `dbmeta` reads it from the server rather than from
the URL: `models/mysql` sets the `mariadb` key when `SELECT VERSION()` carries
the suffix, so a caller passes the `mysql` dialect for both products and never
has to say which. A product that speaks another product's protocol is not a
flavor. From dburl v0.36.0, `cockroachdb`, `cratedb`, `redshift`, `memsql`,
`tidb` and `vitess` each arrive with a dialect of their own, and a model for
one of them shares the statements of the model it imitates where they answer
(D123, D125).

`Query.Support` decides whether to offer a command. It returns `NotBuilt` when
the model was left out of the binary by a build tag, `NotSupported` when the
database has no such object, and `TooOld` when the product has the object and
this release of it does not. Those are three different messages to a person.

Rendering stays in `usql`. D5 keeps the `tblfmt` writer there, so the loop is
to read rows from a `Query` and hand them to the existing writer. The old
`Reader` and `Writer` interfaces do not carry over. `usql` writes no new
reader until it reads `dbmeta` (usql D3).

---

### CONTAINERS (`docs/CONTAINERS.md`)

# Adding a Container or a Machine

This document tells you how to add a server that `dbrun` can start: a new
release of a product that is already listed, a new product, an image that this
repository builds, or a virtual machine. [`DBRUN.md`](DBRUN.md) tells you how
to use `dbrun` once the entry exists, and its rules for a shared machine apply
to every step here.

A server that `dbrun` starts is data in the Go package `container`. It is the
only list of the releases that `dbrun` starts, whether dbmeta is tested
against them or they are Staged. `dbrun` and the CI
workflow both read it, and tests fail when either one drifts from it (D42,
D69). The package starts nothing and imports no container client.

Adding a server is one step of adding a database. [`DIALECT.md`](DIALECT.md)
holds the rest, and its step 4 points here. Another repository that needs a
server, such as `dbimp` or a driver, gets its entry here too, through a dbmeta
session or through Ken. Such an entry is Staged while no model reads it, so
dbmeta's CI does not run it, and the other repository runs it with
`dbrun test staged` or by name (D119).

## Rules

1. Add the entry to `container/` before anything else that needs the server.
   Nothing else starts one (D68).
2. A new product needs Ken's agreement first. D66 holds the order for a dbmeta
   model, and dbimp's order holds it for a server that one of its drivers
   needs (D112, D113). D90 says which products qualify.
3. Choose the releases with [`EVALUATION.md`](EVALUATION.md), and record the
   floor, the ceiling and which step decided in the doc comment of the file.
4. Name every release in one of the tiers `Tested`, `Nightly`, `Verified` or
   `Staged`. CI runs the first two, and a person runs the third before a
   release. A release that no model reads is `Staged`, and CI never runs it.
   Add it with `staged` rather than `add`, and give it the cadence that it
   takes when a model reads it, `Tested`, `Nightly` or `Verified`. dbimp runs its
   Staged releases by that cadence, and it becomes the tier when the model
   arrives (D120).
   `TestAReleaseIsStagedExactlyWhenNoModelReadsIt` holds that a release is
   Staged exactly when no model reads it (D119).
5. Publish one port, the one the tests connect to. If a client needs a second
   interface of the same server, set `second` to its port inside the
   container. dbrun publishes it on the first host port plus 1000, and the
   URL computes it with `container.SecondHostPort`. QuestDB does this for its
   PostgreSQL interface, and Pinot for its controller (D124). `dsn --json`
   prints the address in the `secondAddress` field. Any other interface, such as Couchbase
   Analytics, is not reachable from the host.
6. Use `container.Password` for the administrator. Use another password only
   when the product cannot take one at start, and say why in the file.
7. Give the server `container.MemoryLimit`, which is 4 GB. Ask for more only
   after you measured the product needing it, and write the measurement beside
   the number, as SAP HANA does.
8. Write a readiness check that passes only when a test can connect and log
   in as the user the tests use.
9. Make the setup, `Init`, safe to run twice, because `start` runs it every
   time. Check before you create. `start` also runs it again after a failure,
   up to three times (D105).
10. Pin an image by its digest as well as its tag when somebody other than the
    vendor built it (D88).
11. Read an image without starting a container. Use
    `podman image inspect <image>` for its entrypoint, ports and environment,
    and `podman unshare sh -c 'podman image mount <image>'` for its files.
12. Before you finish, start the server, stop it, start it again, and remove
    it. A second start is a different path from the first. It found two
    faults in the Couchbase entry (D96).
13. Check the manifest format of an image that somebody other than the
    vendor pushed. CI runs docker, and the Docker on GitHub's runners refuses
    a Docker image manifest of schema 1, which podman still pulls. Such an
    image is copied into `docker.io/usql` with
    `podman push --format v2s2`, which Ken does, because it needs his login
    (D100).

## A new release of a product

1. Find the tag on the registry and check that the image is still rebuilt,
   with the command in step 2 of `EVALUATION.md`. If the product's images are
   never rebuilt, apply the rule under step 2 for such a product instead
   (D112).
2. Add the release to the list of its product in `container/<product>.go`,
   with its tier. `list.add` takes the tier and the releases, and `list.on`
   changes one release where the product differs from itself.
3. If the release moves the floor or the ceiling, update the doc comment of
   the file and the row in `EVALUATION.md`.
4. If `README.md` has a tier table for the product, add the release to it.
   `TestTheReadmeTierTablesMatchTheList` fails until you do.
5. Run the checks under Check it.

Do not edit the CI workflow. Its matrix comes from the list (D69).

A release added to the list moves the host port of every server after it,
because a port is 55000 plus the place of the server in `container.All`. A
container that another session built before your change keeps its old port.
`dbrun start` rebuilds it and `dbrun status` reports it, so tell the person
that the ports moved.

## A new product

1. Ask Ken whether the product is next.
2. Choose its releases with `EVALUATION.md`, and add a row to the table
   Databases evaluated so far. `TestEveryProductIsEvaluated` fails until the
   row exists.
3. If dburl names a scheme for the product, add a constant to the `Dialect`
   block in `dialect.go`, with the dburl driver name, if dbmeta has none yet.
   If the server is for a dbimp driver whose name dbimp has not settled,
   leave `dialect` empty. `dbrun` then names the test variable for the
   product, such as `DBMETA_PINOT` (D112).
4. Read the image, as rule 11 says. Find the port the tests use, the
   administrator, how a password reaches it, the settings the tests need, and
   the tools inside it that a readiness check or a setup can run.
5. Write `container/<product>.go`. It holds a doc comment, the `product`
   value, and the list of releases. The field reference below says what each
   field is for, and `container/couchbase.go` is a short example with a setup.
6. Add the list to `All` in `container/container.go`, at the end, so that no
   running server gets a new port.
7. If the product has no shell to run a readiness check, leave `ready` empty.
   `dbrun` then connects from the host through the driver in the `drivers`
   map in `test/cmd/dbrun/test.go`, which needs an entry for the dialect and
   a dbmeta model, because it runs the version query. A product with no model
   needs a `ready` command.
8. Add the administrator to the credentials table in `DBRUN.md`. If the
   setup, the start command or the image creates an ordinary user, declare it
   in `users`, so that `dsn --json` prints it. `container/couchbase.go` does
   this.
9. Set `client` to the native command line client of the product, or to
   `none` with the reason. `TestEveryServerNamesAClientOrSaysWhy` fails until
   it is set. Put the password in an environment variable of the client where
   the client has one, and add the product to `argvPassword` in
   `container/client_test.go` and in `test/cmd/dbrun/cli_test.go` only when it
   has none (D236).
10. Run the checks under Check it.
11. Write a decision in `decisions/` for anything you chose rather than found,
    such as a setting you turned on or a user you created.

## An image this repository builds

Build an image only when the published one cannot do what the tests need and
no setting or argument can change it. Cassandra's image refuses a user
defined function, a materialized view and a role, and Oracle publishes no free
19c image, so both are built. D118 added more: an image with no shell, such as
the Cloud Spanner emulator's, a product that publishes a jar and no image,
such as H2 and Fuseki, and two programs that must run in one container, such
as PostgREST with PostgreSQL and ksqlDB with Kafka. D145 added Impala, whose
four Apache images are built into one.

1. Write `test/cmd/dbrun/image/<product>.Containerfile`. It takes the release
   as the build argument `RELEASE`. Check the result in the build itself, as
   the Cassandra file does, because a `sed` that matches nothing changes
   nothing and reports nothing.
2. Name the image `localhost/dbmeta/<product>` in the entry. `dbrun` embeds
   every file in that directory and builds the one named for the product,
   so nothing else changes. `TestEveryContainerfileHasItsImage` fails when a
   file and its entry disagree. Oracle 19c keeps `localhost/oracle/database`,
   the name Oracle's build script gives it, and has its own build.
3. Keep the password out of the Containerfile. Package the program there,
   and configure it in the entry's command, where `container.Password` is.

`start` and `test` build a missing image. `dbrun build <name>` builds it again,
which is what you run after you change the file.

## A virtual machine

A machine is a `container.Machine`. `dbrun` provisions it once and keeps it,
because building it takes an hour or more. Its tier is always `Verified`,
because CI cannot run one. It has a fixed port of its own, and its screen
viewer has a second.

For a SQL Server release that has no Linux image, 2008 R2 to 2016, follow
[`WINDOWS.md`](WINDOWS.md). The entry is a `WindowsSpec` in
`container/windows.go`, and `dbrun provision <name>` builds it (D57).

For a vendor's appliance, such as the Exasol Community Edition, the entry is an
`ApplianceSpec` in the product's own file. It names the file the vendor
publishes, its SHA-256, the page to download it from, the disks in it, and
its memory and processors. Nothing here can download it, so a person does, and
`dbrun provision <name> --from <file>` checks and imports it (D85, D86).

Name every Verified release in `COVERAGE.md`.
`TestEveryVerifiedReleaseIsDocumented` fails until you do.

## An embedded database

An embedded database has no entry in `container/`, because it has no server
(D42). A model that declares itself embedded gets a `dbrun` entry by itself.
If the database has no model yet, add it to `unmodeled` in
`test/cmd/dbrun/target.go`, with its dialect in `dialect.go` as dburl names
it, and remove it from there when its model is written (D116). If it starts
from sample files, as csvq does, put them in `test/cmd/dbrun/sample/<name>/`.

## Field reference

The fields of `product` in `container/container.go`, which `list.add` copies
into each release:

| Field | What it holds |
| --- | --- |
| `dialect` | the dbmeta dialect, which is the dburl name. It is empty until dbimp settles it, and `dbrun` then names the test variable for the product (D112) |
| `also` | every other dialect the server answers. dbrun sets the test variable of each to the same DSN (D114) |
| `name` | the product, the first half of every server name |
| `image` | the image without its tag, with the registry written out, such as `docker.io/library/postgres` |
| `major` | turns a release into the name a person uses. Only Oracle needs it. |
| `tagPrefix` | text before the release in the tag, such as `v` for SurrealDB |
| `tagSuffix` | text after the release in the tag, such as `-latest` for SQL Server |
| `port` | the port inside the container that the tests connect to |
| `second` | a second port inside the container, which dbrun publishes on the first host port plus 1000, and zero when there is none. QuestDB and Pinot set it (D124) |
| `env` | the environment the image needs, such as its password |
| `ready` | a command, run inside the container, that exits 0 once the server answers |
| `init` | a command, run inside the container on every start after `ready` passes |
| `initInput` | text sent to `init` on its standard input, for an image with no shell, such as SurrealDB's |
| `runFlags` | flags for the run command, before the image name, such as `--entrypoint` where Apache Pinot, Apache Druid and Avatica replace the image's entrypoint, or `--add-host` where Databend blocks its telemetry (D112) |
| `args` | arguments after the image name, for the image's entrypoint or the one `runFlags` names |
| `memory` | a memory limit above `MemoryLimit`, with its measurement |
| `license` | the path inside the container where the product reads its license file, for a product that does not start without one. dbrun mounts the file from the host and lists the product only while it finds the file. Stardog, GraphDB and VoltDB set it (D118) |
| `startup` | a wait longer than 90 seconds, with its measurement |
| `settle` | how long `ready` has to keep passing. Presto and Trino need it (D83). |
| `client` | the native command line client, which `dbrun cli` runs: `psqlClient()` and its relatives for a client, or `none("reason")` for a product that has no shell. Every product sets it (D236) |
| `dsn` | builds the connection string the Go driver takes, for a host port |
| `url` | builds the dburl URL, where it differs from the DSN |
| `users` | the ordinary users that the setup, the start command or the image creates, as `Principal` values with their own DSN, which `dsn --json` prints after the administrator (D102) |

## Readiness checks and setups that went wrong

Each of these was a real fault. The right form is what the entry does now.

| Wrong | What happened | Right |
| --- | --- | --- |
| Couchbase's check asked `/pools` for a 200 | An initialized cluster answers 401 without credentials, so a server started a second time never became ready. | Accept 200 or 401, which says the cluster manager answers. |
| Vertica's check logged in as `dbadmin` on the local socket | It passed before the entrypoint created the user the tests use, and the first connection was refused. | Connect over TCP as that user. |
| Cassandra's check connected without a password | With `PasswordAuthenticator` the server accepts a connection before it can check one. | Log in. |
| Couchbase's setup waited for `SELECT 1` | On a second start the query service answered before the bucket was warm, and 7.2 refused an INSERT. | Wait for a statement that reads the bucket. |
| ScyllaDB's salted password was passed as it is | The entrypoint writes its arguments into a file that a shell reads, and the shell took each `$` as a variable. | Escape each `$` once. |
| SAP HANA ran with `--ulimit nofile=1048576:1048576` | Rootless podman refuses a limit above the host's hard limit, and the container already had it. | No flag. |
| ScyllaDB 2026.3 was expected to create `cassandra` | It creates no default superuser. | Name the superuser at start with `--auth-superuser-name`. |
| SurrealDB stored its data in `/data` | The image runs as a user that cannot write `/data`, and RocksDB refused to start. | Store it in `/tmp`, in the container's own layer, which keeps it across a stop. |
| SurrealDB's setup was going to run `surreal sql` | It exits 0 when a statement fails, so a failed setup reads as a success. | Run `surreal import` on `/dev/stdin`, which exits 1. |

## Check it

Run the tests of the list from the root of the repository, and then the rest
from the `test` directory:

```bash
go test ./container/...
cd test
go run ./cmd/dbrun list <product> --releases
go run ./cmd/dbrun start <name>
go run ./cmd/dbrun stop <name>
go run ./cmd/dbrun start <name>
go run ./cmd/dbrun test <name>
go run ./cmd/dbrun remove <name>
```

Then run the checks in `AGENTS.md` under Before you commit.

## The tests that catch a skipped step

| Test | Fails when |
| --- | --- |
| `TestEveryProductIsEvaluated` | a product has no row in `EVALUATION.md` |
| `TestWorkflowReadsTheList` | the workflow stopped reading the list |
| `TestEveryServerNamesItsPrincipals` | a server does not name its administrator, or an ordinary user's DSN does not hold that user's name |
| `TestWorkflowImagesAreQualified` | an image in the workflow has no registry written out |
| `TestWorkflowImagesAreInTheList` | the workflow names an image that the list does not |
| `TestTheReadmeNamesEveryTier` | the list has a tier that `README.md` does not explain |
| `TestTheReadmeTierTablesMatchTheList` | `README.md` puts a release in a tier the list does not |
| `TestEveryVerifiedReleaseIsDocumented` | a Verified release is not named in `COVERAGE.md` |
| `TestEveryMachineIsUsable` | a machine shares a name or a port with another server, lacks its dialect, product or release, or is not Verified |
| `TestAReleaseIsStagedExactlyWhenNoModelReadsIt` | a release that no model reads is not Staged, or one that a model reads is (D119) |
| `TestEveryContainerfileHasItsImage` | a Containerfile and the entry that names its image disagree (D118) |
| `TestEveryWindowsMachineIsUsable` | a Windows machine is missing what its install needs |

---

### COVERAGE (`docs/COVERAGE.md`)

# What Each Database Can Answer

`dbmeta` asks every database the same 65 questions. PostgreSQL answers all of
them, because PostgreSQL is the model. No other database answers all of them,
and this document says which ones each database answers, which ones it cannot,
and why.

Fifty eight of the questions come from `psql`. The other seven exist because a
consumer needs them and `psql` has no command for them: the columns of a
constraint, the parameters of a routine, the labels of an enumerated type, the
statement a view selects, the statistics over a column, the schema an
unqualified name resolves in, and the user the connection is authenticated as.
D47 sets the rule for adding one, and D55 added the last.

A question that a database cannot answer returns `dbmeta.ErrNotSupported`. It
never returns an empty result. Those are different facts and a caller has to be
able to tell them apart: an empty result means the server holds none of that
object, and `ErrNotSupported` means the server has no such object at all.

Read `COMMANDS.md` for the `psql` command that each Go value answers. Read
D43 in `decisions/` for the rule about finding these analogues in the first place.

Adding a database means adding a section here. [`DIALECT.md`](DIALECT.md) holds
every step and this is one of them, so read that first if you are adding one
rather than reading one.

## The count

| Model | Answers | Of | Tested against |
| --- | --- | --- | --- |
| `models/postgres` | 65 | 65 | PostgreSQL 9.6 through 18 |
| `models/mysql` | 30 on MariaDB, 27 on MySQL | 65 | MariaDB 10.6 to 13.0, MySQL 8.4 to 26.7 |
| `models/sqlite3` | 14 | 65 | both drivers: mattn/go-sqlite3 and modernc.org/sqlite |
| `models/duckdb` | 21 | 65 | duckdb/duckdb-go, the driver usql uses |
| `models/sqlserver` | 34 | 65 | SQL Server 2017, 2019, 2022 and 2025 |
| `models/oracle` | 29 | 65 | Oracle 11g, 18c, 19c, 21c, 23ai and 26ai |
| `models/cassandra` | 17 on Cassandra, 18 on ScyllaDB | 65 | Cassandra 3.11, 4.0, 4.1 and 5.0, ScyllaDB 2025.1, 2026.1, 2026.2 and 2026.3 |
| `models/clickhouse` | 25 | 65 | ClickHouse 25.3, 25.8, 26.8 and 26.9 |
| `models/trino` | 13 | 65 | Trino 476 and 484 |
| `models/presto` | 9 | 65 | Presto 0.299 |
| `models/firebird` | 25 | 65 | Firebird 3.0, 4.0 and 5.0 |
| `models/hana` | 33 | 65 | SAP HANA 2.00.076, 2.00.082 and 2.00.088, which are SPS 07 and SPS 08 |
| `models/hive` | 18 | 65 | Apache Hive 4.0 and 4.2 |
| `models/exasol` | 26 | 65 | Exasol 2026.2.0 on the nano image, and 2025.2.1 on the Community Edition machine |
| `models/vertica` | 28 | 65 | Vertica 7.2.1, 9.1.0, 10.1.1 and 25.1.0, on copies of community images in `docker.io/usql/vertica` |
| `models/couchbase` | 12 | 65 | Couchbase 7.6.12 and 8.0.3, and 7.2.9, which is Tested and refused as too old |
| `models/cockroachdb` | 54 | 65 | CockroachDB 24.3.36, 26.2.8 and 26.3.2. 47 of its statements are the postgres model's (D123) |
| `models/cratedb` | 27 | 65 | CrateDB 6.3.7 and 6.4.6, where 6.3 answers one fewer, collations. 3 of its statements are the postgres model's (D123) |
| `models/questdb` | 11 | 65 | QuestDB 9.4.3 and 10.0.1, on the PostgreSQL interface with pgx |
| `models/tidb` | 20 | 65 | TiDB 7.5.8, 8.1.2 and 8.5.8, where privileges needs 8.5. 17 of its statements are the mysql model's (D133) |
| `models/vitess` | 21 | 65 | Vitess 23.0.8 and 24.0.5, on vttestserver. 20 of its statements are the mysql model's, and a schema is a keyspace (D135) |
| `models/databend` | 20 | 65 | Databend 1.2.881 and 1.2.954, from the system database, with dbimp's driver (D140) |
| `models/singlestore` | 23 | 65 | SingleStore 9.0 and 9.1, on the development image with no license. 16 of its statements are the mysql model's (D141) |
| `models/snowflake` | 15 | 65 | measured on a Snowflake trial account, release 10.36.101, on 2026-10-08 and 2026-10-09. Written before an account existed (D144) and corrected by D190. Parity, conformance and the password statement were measured by D193, and D203 reads the columns of a key. Since D213 the tests and `dbrun` connect through the driver of dbimp, which is the one dburl v0.49.0 names |
| `models/redshift` | 18 | 65 | measured on Redshift Serverless 1.0.434008 on 2026-10-08 and 2026-10-09. Written before a cluster existed (D144) and corrected by D182 and D204, and it reads the Spectrum external tables (D221) |
| `models/impala` | 11 | 65 | Apache Impala 4.4.1 and 4.5.3, in one container dbrun builds. Most kinds are a walk of SHOW statements (D146) |
| `models/neo4j` | 17 | 65 | Neo4j 2026.10.0, and 5.26.32, which is too old for four of them because a SHOW command cannot be joined with other clauses. With dbimp's driver (D162) |
| `models/influxdb` | 9 | 65 | InfluxDB 3 Core 3.10.6, 3.11.7 and 3.12.1, from DataFusion's information_schema, with dbimp's driver (D152) |
| `models/ydb` | 7 | 65 | YDB 26.2.1.14 and 26.3.1.19, from the .sys views, with ydb-go-sdk (D161) |
| `models/influxql` | 7 | 65 | InfluxDB 1.11.8 and 1.13.1, and 2.8.0 and 2.9.1, which answer 4 of the 7. Most kinds are a walk of SHOW statements, with dbimp's driver (D159, D165) |
| `models/surrealdb` | 18 | 65 | SurrealDB 3.1.6, 3.2.6 and 3.3.4, and 2.7.0, which answers the current schema alone, because a 2.x statement cannot read INFO as a value. With dbimp's driver (D164) |
| `models/rqlite` | 14 | 65 | rqlite 9.4.5 and 10.6.3, with dbimp's driver. Every statement is the sqlite3 model's (D148, D151) |
| `models/libsql` | 14 | 65 | libSQL 0.24.33, the sqld server, with dbimp's driver. Every statement is the sqlite3 model's, with a fragment for the vector index (D160) |
| `models/arangodb` | 7 | 65 | ArangoDB 3.12.12, in AQL through dbimp's driver. A database is the schema, a collection is a table, and its JSON schema rule gives its columns (D163, D168) |
| `models/druid` | 7 | 65 | Apache Druid 37.0.0 and 38.0.0, from INFORMATION_SCHEMA and sys, with dbimp's driver. The version and the settings need an administrator (D171) |
| `models/drill` | 10 | 65 | Apache Drill 1.21.2 and 1.22.0, from INFORMATION_SCHEMA and sys, with dbimp's driver. A file table is listed only when the Metastore is on (D178) |
| `models/elasticsearch` | 8 | 65 | Elasticsearch 8.19.23, 9.4.8 and 9.5.5, from SYS and SHOW statements that a walk reads, with dbimp's driver. The release comes from `SELECT version()`, which every user can read (D177, D191, D192) |
| `models/opensearch` | 3 | 65 | OpenSearch 3.9.0 and 2.19.7, from SHOW TABLES and DESCRIBE that a walk reads, with dbimp's driver at v0.14.1. The release comes from `SELECT version()`, which every user can read (D181, D189, D191, D192) |
| `models/solr` | 4 | 65 | Apache Solr 9.9.0, 9.10.1 and 10.0.0, from metadata.TABLES and metadata.COLUMNS, with dbimp's driver. The release comes from `SELECT version()`, which every user can read (D179, D191, D192) |
| `models/gizmosql` | 21 | 65 | GizmoSQL 1.41.0 and 1.42.0, where DuckDB 1.5.6 was measured on 1.40.0 and 1.41.0, with the Arrow Flight SQL driver. Every statement is the duckdb model's (D187) |
| `models/spanner` | 22 | 65 | Spanner Omni 2026.r4-lts and Cloud Spanner, in the GoogleSQL dialect. Cloud Spanner is tested with the driver of dbimp, and Omni with go-sql-spanner in a binary of its own (D216, D219, D231) |
| `models/bigquery` | 18 | 65 | the hosted Google BigQuery service, which has no release, in the GoogleSQL dialect, with the driver of dbimp (D220, D226, D229) |
| `models/athena` | 8 | 65 | the hosted Amazon Athena service, which has no release that SQL reads, in the Trino based dialect of engine version 3, with the driver of dbimp, which binds with ExecutionParameters (D222, D229) |
| `models/databricks` | 17 | 65 | the hosted Databricks SQL service on Unity Catalog, which reports the release of its SQL channel, 2026.38, with the driver of dbimp (D224, D229) |
| `models/cosmos` | 15 | 65 | the hosted Azure Cosmos DB account with the API for NoSQL, which has no release, with the driver of dbimp. A statement is a SELECT against one of twelve reserved names that the driver answers with REST requests. The five newest need the dbimp tag after v0.19.0 (D228, D233) |
| `models/avatica` | 24 | 65 | the standalone Avatica server 1.28.0 and 1.29.0, which is Avatica over HSQLDB 2.4.1, from INFORMATION_SCHEMA and the SYSTEM_ views, with dbimp's driver. Phoenix has no model (D186) |
| `models/informationschema` | 12 | 65 | any database with a standard `information_schema` |

The shared `information_schema` model answers twelve: tables, schemas,
columns, functions, privileges, constraints, sequences, constraint columns,
routine parameters, views, the current schema and the current user. It is the floor. A native model
exists to beat it, and `models/mysql` beats it by seventeen.

Four of those twelve arrived with the kinds D47 added, and they arrived for
free: the standard defines `key_column_usage`, `parameters`, `views` and
`schemata`, so every database close to the standard answers them.

## PostgreSQL

PostgreSQL answers all 65 questions on release 18, and 64 on 10 to 17, where
`NotNulls` is refused because the release does not name a NOT NULL
constraint. Release 9.6 answers 57. It also refuses publications, publication
tables, subscriptions, subscription connections, statistics objects,
partitioned tables and partitions, which arrived in release 10. Fields that
psql prints for the describe commands arrived in D198, D199 and D201, and a
release below the one that has the source answers NULL for them.

| Field | Source | Filled from |
| --- | --- | --- |
| `Table.Owner`, `Table.Persistence`, `Table.Size`, `Table.Rows` | `pg_class` | every release |
| `Table.AccessMethod` | `pg_class.relam` and `pg_am` | 12. NULL for a view and a sequence |
| `Index.Owner`, `Index.Persistence`, `Index.Size` | `pg_class` of the index | every release |
| `Index.Predicate`, `Index.Valid`, `Index.Clustered`, `Index.ReplicaIdentity` | `pg_index` | every release |
| `Index.Deferrable`, `Index.InitiallyDeferred` | `pg_constraint`, for a primary key, unique or exclusion constraint | every release. NULL for an index no constraint owns |
| `Column.Storage`, `Column.StatsTarget` | `pg_attribute` | every release. `StatsTarget` is NULL for the default |
| `Column.Compression` | `pg_attribute.attcompression` | 14. NULL for the default |
| `Function.Leakproof`, `Function.Prosrc` | `pg_proc` | every release |
| `Table.Options`, `Index.Options` | `reloptions`, and the TOAST options with a prefix | every release |
| `Table.RowSecurity`, `Table.RowSecurityForced` | `relrowsecurity`, `relforcerowsecurity` | every release |
| `IndexColumn.Include` | `indnkeyatts` | 11. False below it |
| `PartitionedTable.Table`, `PartitionedTable.AccessMethod` | `pg_index` and `pg_am` | 10 |
| `PartitionedTable.DirectSize`, `PartitionedTable.TotalSize` | `pg_partition_tree` | 12 |
| `PublicationTable.Via` | the publication of the table, of its schema or of every table | 10, and 15 for a schema |
| `ExtendedStat.StatsTarget` | `stxstattarget` | 13 |
| `Partitions` | `pg_inherits`, `pg_get_expr` and `pg_get_partition_constraintdef` | 10 |
| `Inherits` | `pg_inherits` | every release |
| `Policies` | `pg_policy` | 9.5. `Permissive` is true below 10. `Enabled` is NULL, because PostgreSQL switches row security for a table and not for one policy (D211) |
| `Rules` | `pg_rewrite` and `pg_get_ruledef` | every release |
| `NotNulls` | `pg_constraint` with the type n | 18 |
| `Schema.Access` | `nspacl` | every release. NULL for the default privileges |
| `Extension.DefaultVersion` | `pg_available_extensions()` | every release. NULL when the control file is not on the server |
| `Database.LocaleProvider` | `datlocprovider` | 15. Below it libc, which is the only provider there was |
| `Database.Locale` | `daticulocale`, and `datlocale` from 17 | 15. NULL for libc |
| `Database.ICURules` | `daticurules` | 16 |
| `Operator.Leakproof` | `proleakproof` of the function of the operator | every release. False for an operator with no function |
| `Cast.LeakProof` | `proleakproof` of the function of the cast | every release. NULL for a cast with no function, which is binary coercible or with inout. The field held its value before D201 |
| `Publication.GeneratedColumns` | `pubgencols` | 18. Below it none, because none was published |
| `Subscription.Binary` | `subbinary` | 14 |
| `Subscription.Streaming` | `substream`, as off, on or parallel | 14, and parallel from 16 |
| `Subscription.TwoPhase`, `Subscription.DisableOnError`, `Subscription.SkipLSN` | `subtwophasestate`, `subdisableonerr`, `subskiplsn` | 15 |
| `Subscription.Origin`, `Subscription.PasswordRequired`, `Subscription.RunAsOwner` | `suborigin`, `subpasswordrequired`, `subrunasowner` | 16 |
| `Subscription.Failover` | `subfailover` | 17 |
| `SubscriptionConnections` | `subconninfo` | 10. Refused for a role that is not a superuser |
| `TextSearchParserFunctions` | `pg_ts_parser` and the comments of its functions | every release |
| `TextSearchDictionary.TemplateSchema` | `pg_ts_template` | every release |
| `OperatorFamilyOperator.LeftType`, `RightType`, `SortFamily`, `Leakproof`, `Visible`, `FamilySchema` | `pg_amop`, `pg_operator` and `pg_proc` | every release |
| `OperatorFamilyFunction.FunctionName`, `Visible`, `FamilySchema` | `pg_amproc` and `pg_proc` | every release |
| `OperatorClass.Visible`, `FamilyVisible`, `FamilySchema`, and the `type` parameter | `pg_opclass_is_visible` and `pg_opfamily_is_visible` | every release |
| `Sequence.CacheSize` | `seqcache` | 10 |
| `Index.Definition`, `Index.Using` | `pg_get_indexdef` | every release |
| `Index.ConstraintType`, `Index.ConstraintDefinition` | `pg_constraint` and `pg_get_constraintdef`, the join that gives `Deferrable` | every release. NULL for an index no constraint owns |
| `Index.ConstraintPeriod` | `conperiod` | 18. False below it, for an index that a constraint owns |
| `ForeignOptions` | `pg_options_to_table` over the options of the wrapper, the server, the user mapping and the foreign table | every release. A user mapping gives no row to a role that cannot read its options |
| `ColumnPrivileges` | `attacl` and `aclexplode` | every release |
| `Partition`, `Inherit`, `PartitionedTable`, `ExtendedStat` and `Index` flags that end in `Visible` | `pg_table_is_visible` | every release the kind exists on |

`PartitionedTable.Type` and `Privilege.Type` say `partitioned table` for a
partitioned table, and `PartitionedTable.Type` says `partitioned index` for a
partitioned index (D201).

`Table.Rows` is the estimate in `reltuples`. It is 0 before the first analyze
on a release below 14 and -1 from 14. `Table.Type` says `partitioned table` for
a partitioned table (D198).

## MariaDB and MySQL

These are two products sharing one dialect and one model. Most of what follows
is true of both. Where they differ, the model gates on the product rather than
on the release number, because MariaDB is at 11.8 and MySQL at 9 and neither
number says anything about the other. See D44.

MariaDB answers 30 of the 65 and MySQL answers 27. The three MySQL cannot
answer are sequences, which it has never had, aggregates, which it has no form
of and whose catalog table it dropped in 8.0, and column statistics, below.

### What it answers with the same thing PostgreSQL has

Schemas, databases, tables, columns, indexes, index columns, constraints,
constraint columns, triggers, sequences, views, partitioned tables, comments,
functions, routine parameters, collations, settings, roles, role grants,
privileges, column statistics, the current schema and the current user.

A schema and a database are the same object in MariaDB. `dbmeta.Schemas` and
`dbmeta.Databases` both answer, and they answer with the same rows. That is the
one place the object model does not fit, and it is not hidden.

### The fields that D205 fills

D198, D199 and D203 added fields and kinds that only PostgreSQL filled. D205
reads each of them from the catalog of the MySQL family and leaves NULL where
the catalog has no source. Every field below is a column of a row that the
statement already reads, so the cost is the cost of the row.

| Field | MariaDB | MySQL | Left NULL because |
| --- | --- | --- | --- |
| `Table.Persistence` | `permanent`, and `temporary` for a temporary table of the session, which `TABLE_TYPE` names TEMPORARY | `permanent`, because a temporary table is not in the catalog | TiDB lists a global temporary table as a base table, so TiDB answers NULL |
| `Table.AccessMethod` | `TABLES.ENGINE`, NULL for a view | the same | SingleStore names its one engine MemSQL, so it reads `STORAGE_TYPE`, which is COLUMNSTORE or INMEMORY_ROWSTORE |
| `Table.Size` | `DATA_LENGTH + INDEX_LENGTH` in bytes, NULL for a view | the same | |
| `Table.Rows` | `TABLE_ROWS`, an estimate | the same | |
| `Table.Options` | `CREATE_OPTIONS`, such as `row_format=COMPRESSED key_block_size=4`, or `partitioned`. The separator is a space | the same | SingleStore and TiDB keep none |
| `Table.Owner` | | | no product of the family records an owner of a table |
| `Index.Using` | `STATISTICS.INDEX_TYPE`: BTREE, HASH, FULLTEXT, SPATIAL | the same | |
| `Index.Size` | | | `mysql.innodb_index_stats` is a table of the mysql schema and a lesser principal cannot read it, and the INNODB views need PROCESS |
| `Index.Clustered` | | | `INNODB_SYS_INDEXES` and `INNODB_INDEXES` need PROCESS, which a grantee does not hold. Measured on MariaDB 12.3 |
| `Index.Valid` | | | `IGNORED` and `IS_VISIBLE` say that the planner skips an index, which is a choice and not a fault, so they are not `Valid` |
| `Index.Predicate`, `Owner`, `Persistence`, `Options`, `Definition` | | | none. `SHOW CREATE TABLE` is a statement of its own |
| `Column.Compression` | `zlib` for a column made `COMPRESSED`, from 10.3, read from the comment `COLUMN_TYPE` carries | | MySQL compresses a page or a table, which `Table.Options` shows, and never a column |
| `Column.Storage`, `StatsTarget` | | | none |
| `Constraint.Enforced` | true. There is no `ENFORCED` column, and MariaDB checks every constraint it records, so the value is a fact of the product (D212) | `TABLE_CONSTRAINTS.ENFORCED` from 8.0.16, NULL below | TiDB, SingleStore and Vitess are as before |
| `Function.Prosrc` | the body, which is `Source` | the same | |
| `Partitions` | new. One row for each partition and each subpartition, from `PARTITIONS`. `Bound` is `PARTITION_DESCRIPTION`: the upper limit of a range, the values of a list, NULL for hash and key | the same | `Constraint` is NULL, because a partition has no constraint text |
| `PartitionedTables.DirectSize`, `TotalSize` | `SUM(DATA_LENGTH + INDEX_LENGTH)` of the partitions. They are equal, because a partition holds its subpartitions | the same | |
| `Policies`, `NotNulls`, `Inherits` | | | the family has none of them (D43) |

Gemini and DeepSeek were asked on 2026-10-10, as rule 14 requires. Both named
`mysql.innodb_index_stats` for the index size, and the `INNODB_INDEXES` view
(`INNODB_SYS_INDEXES` on MariaDB) for the clustered flag, and said that no
view holds a compression method of a column or an owner of a table. DeepSeek
said that MariaDB writes the column compression into `COLUMN_TYPE`, and the
run confirmed it. I ran both leads as a user with `SELECT` on one schema, and
both views answered `Access denied; you need the PROCESS privilege`, so the
two fields stay NULL. `TABLE_ROWS`, `DATA_LENGTH` and `INDEX_LENGTH` of a
view are NULL on MariaDB and MySQL, and `Table.Size` and `Rows` keep that.
`ENFORCED` is not a column of `TABLE_CONSTRAINTS` on MariaDB 10.6 to 13.0, and D212 fills it with true.

### What it answers with an analogue

| Question | What MariaDB reads | Why it is a fair answer |
| --- | --- | --- |
| `AccessMethods` | `information_schema.ENGINES` | A storage engine decides how a table is stored and searched, which is what an access method decides. `psql` prints the access method of a table in `\d`, and the engine is what belongs there. |
| `Extensions` | `information_schema.ALL_PLUGINS` | A plugin adds capability to a running server and can be loaded and unloaded, as an extension can. |
| `Aggregates` | `mysql.proc` where `aggregate` is `GROUP`, and `mysql.func` where `type` is `aggregate` | These are aggregates, created with `CREATE AGGREGATE FUNCTION`. The two tables hold the two forms, stored and compiled. |
| `ForeignServers` | `mysql.servers` | `CREATE SERVER` names a remote server and its wrapper, which is what `CREATE SERVER` does in PostgreSQL. |
| `UserMappings` | `mysql.servers` | A MariaDB server carries one credential and every local user reaches the remote server with it. That is the mapping PostgreSQL writes for `PUBLIC`. |
| `ForeignTables` | `information_schema.TABLES`, filtered on the engine | A table on `FEDERATED`, `CONNECT`, `SPIDER`, `OQGRAPH` or `S3` reads data this server does not hold, which is what a foreign table is. |

Four of these need `SELECT` on the `mysql` schema, because `mysql.proc`,
`mysql.func` and `mysql.servers` are tables rather than views and MariaDB does
not grant them to an ordinary user. PostgreSQL makes the same information
readable by everyone. A caller without that privilege gets an error from the
driver, not from `dbmeta`.

`ForeignTables` reports the engine in the `server` column, because
`information_schema` does not publish the `CONNECTION` setting that names the
server. The field says so.

### Where the two products differ

| Question | MariaDB reads | MySQL reads |
| --- | --- | --- |
| `Constraints`, the check clause | `information_schema.CHECK_CONSTRAINTS` from 10.2, joined on the table as well as the name | the same view from 8.0.16, joined on the schema and the name, because it has no table column |
| `Settings` | `information_schema.SYSTEM_VARIABLES` | `performance_schema.global_variables` on 8.4, which holds the value alone, and `variables_metadata` joined to it from 9, which adds the type and the scope |
| `Roles`, whether it can log in | `mysql.user.is_role` | `mysql.user.account_locked`, because MySQL marks a role by locking the account and has no such column |
| `RoleGrants` | `mysql.roles_mapping`, which names the member and the role it holds | `mysql.role_edges`, which names the role it came from and the account it went to, so the columns are read the other way round |
| `Extensions` | `information_schema.ALL_PLUGINS` | `information_schema.PLUGINS`, which MySQL has instead |
| `Sequences` | `information_schema.SEQUENCES` from 11.5 | nothing: MySQL has no sequences at any release |
| `Aggregates` | `mysql.proc` and `mysql.func` | nothing: MySQL dropped `mysql.proc` in 8.0 and has no aggregate of its own |
| `ColumnStats` | `mysql.column_stats` joined to `mysql.table_stats` | nothing: `information_schema.COLUMN_STATISTICS` holds one JSON histogram and no width, null fraction or distinct count |

`Functions` reads `routine_body` for the language and never
`external_language`. MariaDB leaves `external_language` NULL for a SQL routine
and MySQL writes `SQL`, and the field is not nullable, so reading it fails to
scan on MariaDB. The cross product test found that.

### What the two products answer the same way, and what they spell differently

`TestMySQLAgainstMariaDB` builds the same fixture on one server of each product
and compares every query that narrows to one schema. Fourteen queries compare,
measured on 2026-10-01, and every column agrees except the ones below.

`Columns.DataType` and `Columns.Default`. MariaDB keeps the display width of an
integer and MySQL dropped it, so a column reads `int(11)` on one and `int` on
the other. MariaDB quotes a string default and MySQL does not, so the same
default reads `'red'` and `red`.

`Columns.Collation`. Each product has its own default collation, and a text
column that names none takes it: `utf8mb4_uca1400_ai_ci` on MariaDB from
11.4, and `utf8mb4_0900_ai_ci` on MySQL from 8.0. The compare job in CI
failed on it from the change that added the field until it was named.

`Constraints.Definition`. MariaDB records the check clause as written and MySQL
rewrites it with the character set introducer, so ``` `title` <> '' ``` becomes
``` (`title` <> _utf8mb4'') ```.

`RoutineParameters.DataType`. The same display width difference, reaching a
parameter through `dtd_identifier` rather than a column.

`Views.Definition`. Neither product stores a view as written. Both rewrite it,
and they rewrite it differently: MySQL parenthesizes the `WHERE` clause and
MariaDB does not.

Those four are named in the test. Any other difference fails it, so a query
written for one product and run against the other is caught here rather than by
a user. It has caught three faults so far, the most recent in CI: the
comparison keyed a row by its name and ordinal without the routine it belongs
to, so two routines' return values, both reported with no name at ordinal zero,
looked like one row whose name kept changing.

`COLLECTION_COUNT` on 1500 collections took 10.6 to 12.0 ms against 1.9 to
2.1 ms for the list alone, and 0.85 ms for one collection by name (D212).

### What it cannot answer, and why

MariaDB has no equivalent of any of these. Reporting them unsupported is the
whole answer.

| Question | Why |
| --- | --- |
| `Types`, `Domains` | MariaDB has no `CREATE TYPE` and no `CREATE DOMAIN`. The type of a column is one of a fixed set. |
| `Operators`, `OperatorClasses`, `OperatorFamilies`, `OperatorFamilyOperators`, `OperatorFamilyFunctions` | Operators are built into the parser and cannot be created, so there is nothing to list and no index strategy to group them into. |
| `Casts`, `Conversions` | Conversion is implicit and is not declared, so there is no catalog of it. |
| `Languages` | A stored routine is SQL and nothing else. The `language` column of `mysql.proc` is an enum with one value. |
| `LargeObjects` | A large value is a `LONGBLOB` column, not an object with its own identity. |
| `EventTriggers` | A trigger fires on a row, never on a statement that changes the schema. |
| `RoleSettings`, `DefaultACLs` | A setting belongs to a session or to the server, never to a role. A grant applies to what exists, never to what will be created. |
| `Publications`, `PublicationTables`, `Subscriptions` | MariaDB replicates a binary log, not a named set of tables. There is no publication to name and no subscription to list. |
| `TextSearchParsers`, `TextSearchDictionaries`, `TextSearchTemplates`, `TextSearchConfigs`, `TextSearchConfigMaps` | Full text search is a `FULLTEXT` index with a built in tokenizer. None of its parts is a nameable object, so there is nothing to map either. |
| `ExtensionObjects` | A plugin owns no SQL objects, so there is nothing to list as belonging to one. |

### Analogues that were found and rejected

Two AI models were asked what MariaDB holds for the 29 questions the first pass
did not answer, which is the rule D43 sets. Both named analogues that do not
survive a look at a running server. They are recorded here so that the next
person does not find them again and reach the other conclusion.

`information_schema.TABLESPACES` for `Tablespaces`. Both models named it. The
table exists and it has the right columns, and on MariaDB 11.8 it returns no
rows and can never return any: it is the MySQL Cluster table, kept for
compatibility. `information_schema.INNODB_SYS_TABLESPACES` does return rows,
and each row is one file behind one table, not a named place an administrator
created. A list that names `dbmeta_fixture/book` as a tablespace teaches a
caller something false.

`information_schema.ENGINES` for `ForeignDataWrappers`. An engine is already
the answer for `AccessMethods`, and an engine is not a wrapper: it is not named
by `CREATE SERVER` and a server does not point at one. `mysql.servers.Wrapper`
holds a real wrapper name, but only for wrappers already in use, and `\dew`
lists the wrappers a server has rather than the ones it uses.

`mysql.column_stats`, `mysql.table_stats` and `mysql.index_stats` for
`ExtendedStats`. These hold engine independent statistics, one row per column.
`\dX` lists the statistics objects `CREATE STATISTICS` makes, and the point of
those is that they cover several columns at once. A single column histogram is
a different thing with a similar name.

`information_schema.PLUGINS` with `PLUGIN_TYPE` of `FTPARSER` for
`TextSearchParsers`. A full text parser plugin genuinely is a text search
parser, so this one is close. It was rejected because the built in parser is
not a plugin and does not appear, so a default install answers with no rows
while full text search works. `psql` lists the built in parser for `\dFp`, and
an empty answer here reads as "this server has no full text search".

`mysql.slave_master_info` for `Subscriptions`. MariaDB does not have that
table. It keeps master information in a file, and `SHOW SLAVE STATUS` is the
way to read it, which is not a catalog query.

### Catalogs MariaDB has that no question asks for

These hold real metadata and no `psql` command maps onto them, so `dbmeta` does
not read them. A later question can.

`information_schema.EVENTS` holds scheduled events, which PostgreSQL has no
form of. `information_schema.PERIODS` holds application time periods. It is present on
11.8 and absent on 10.6, so a query for it needs a version gate.

## SQLite

SQLite answers 14 of the 65. It is a small native model and it still beats
the shared `information_schema` one, which SQLite does not have at all.

It is also one of the two databases here with no server, and DuckDB is the
other. SQLite is a library, so the
release under test is whichever one the Go driver was built with. There is
nothing to upgrade separately, nothing to run in a container, and no version
gate in the model: every pragma it reads arrived by SQLite 3.37 in 2021, and
the only way to reach an older one is to pin an old driver on purpose.

Two drivers are tested, as subtests named for each. `mattn/go-sqlite3` compiles
the upstream SQLite source and needs cgo, which is why it is the primary one:
it is the real database. `modernc.org/sqlite` is a pure Go translation and is
what a consumer who cannot use cgo runs. They ship different library versions
in general, and they happen to agree today. See D48.

### What it answers

Eleven from `psql` and three of the six D47 added: the columns of a constraint,
the statement a view selects, and the schema an unqualified name resolves in.
It cannot answer routine parameters, enum values or column statistics, and the
reasons are in the table under The seven kinds psql has no command for.

| Question | What SQLite reads |
| --- | --- |
| `Schemas`, `Databases` | `pragma_database_list`. An attached database is what SQLite calls a schema and it is also the only thing it calls a database, so both answer with the same rows. |
| `Tables` | `sqlite_schema`. A table backed by a module reports its type as `virtual` rather than `table`, because it does not behave like one. |
| `Columns` | `sqlite_schema` joined to `pragma_table_xinfo`. The xinfo form rather than info, so that a generated column and a virtual table's hidden column appear. |
| `Indexes` | `pragma_index_list`, which carries what `sqlite_schema` cannot: whether the index is unique and whether SQLite made it for a constraint. |
| `IndexColumns` | `pragma_index_xinfo`, filtered to the columns the index is on rather than the ones it carries to reach a row. |
| `Constraints` | Three pragmas joined. See below. |
| `Triggers` | `sqlite_schema`. The definition is the whole `CREATE TRIGGER` statement, because SQLite keeps nothing else. |
| `Functions` | `pragma_function_list`, folded to one row per name and kind. |
| `Collations` | `pragma_collation_list`. |
| `Settings` | One `SELECT` per pragma, joined by `UNION ALL`. See below. |

A correlated table-valued join is what makes most of these one statement
rather than one per table:

```sql
FROM sqlite_schema m JOIN pragma_table_xinfo(m.name) c
```

### The one query that answers incompletely

`Constraints` reports primary key, unique and foreign key, and never reports a
check constraint. SQLite records a check constraint only inside the
`CREATE TABLE` text in `sqlite_schema.sql`, and `dbmeta` does not parse DDL.

This is the one place a query here answers partially rather than not at all.
Both Gemini and DeepSeek argued for it and they are right: three kinds read
exactly are worth more than refusing all four over the fourth. The field
description says so, and the test creates two check constraints and asserts
that neither appears, so the gap is tested rather than assumed.

### Settings, which is assembled rather than written

There is no way to ask SQLite for the value of a pragma it names.
`pragma_pragma_list` gives names and no values, and reading a value means
naming the pragma in the SQL. So the query is built at startup from a list of
36 pragmas, one `SELECT` each, joined by `UNION ALL`.

Two things that only running it reveals. A pragma appears in
`pragma_pragma_list` whether or not it can be read as a table, so
`mmap_size`, `wal_autocheckpoint`, `wal_checkpoint`, `case_sensitive_like` and
`temp_store_directory` are left out: they are pragmas and they are not tables.
And `busy_timeout` returns a column called `timeout`, which is the only one in
the list not named after its own pragma.

`compile_options` is left out on purpose. A build flag is not a setting a
caller can change, which is what `\dconfig` means.

### Analogues that were found and rejected

Two AI models were asked what SQLite holds for the 37 questions the first pass
did not answer, which is the rule D43 sets. They agreed on almost all of it,
and every rejection below is theirs as well as mine.

`sqlite_sequence` for `Sequences`. It exists only when a table uses
`AUTOINCREMENT`, and it gets a row only after the first insert. It is a counter
SQLite keeps for itself, not an object anyone created, and nothing can read the
next value from it.

`pragma_module_list` for `Extensions`, `AccessMethods` or
`ForeignDataWrappers`. A module implements a virtual table. It is not an
extension, it is not an index method, and nothing wraps foreign data with it.

A virtual table for `ForeignTables`, the way a `FEDERATED` engine answers for
MariaDB. This one looked right and it is not: a virtual table is also how
SQLite does full text search, JSON and R-trees, so most of what the query
returns is local data.

`pragma_compile_options` for `Settings`, rejected above.

`sqlite_stat1` for `ExtendedStats`. It holds one row per index, which is
PostgreSQL's `pg_statistic`, not the `CREATE STATISTICS` objects that `\dX`
lists.

`pragma_table_xinfo.type` for `Types`. A declared type in SQLite is an
unenforced affinity hint. A column of them presented as a type catalog
suggests a check that does not happen.

`pragma_function_list` type `a` and `w` for `Aggregates`. This is the one the
models split on, and running it settled it against both. Gemini said to map
both and called it exact. DeepSeek said to map only `a`. On a real server,
`sum`, `count`, `avg` and `group_concat` all report as `w`, and so do
`row_number`, `rank` and `lag`. Type `a` matched one function, an extension.
If the query maps `w`, it lists `row_number` as an aggregate. If it maps `a`,
it omits `sum`. Both mislead, there is no third option, and SQLite cannot
tell an aggregate from a window function. `Aggregates` is unsupported and
`Functions` reports the kind SQLite reports.

### What it has none of

SQLite has no users, no roles and no grants of any kind, so `Roles`,
`RoleGrants`, `RoleSettings`, `Privileges` and `DefaultACLs` are absent rather
than empty. It records no comment on any object. It has no type catalog, no
operators that can be created, no casts, no procedural languages, no
replication, no tablespaces and no partitioning.

### The describe fields (D208)

D198 and D199 gave Tables, Indexes and Columns fields that only PostgreSQL
filled. D208 measured each one on SQLite 3.53.4, on both drivers.

| Field or kind | What SQLite fills |
| --- | --- |
| `Table.Persistence` | `permanent` for a table, because the statement reads `sqlite_schema`, which is the main database. NULL for a view. A temporary table is in `sqlite_temp_schema`, which the model does not read |
| `Table.Options` | NULL. `pragma_table_list` has `strict` and `wr`, which is WITHOUT ROWID, and a join to it for every table costs time that grows with the square of the catalog: 0.9 ms at 300 tables, 88 ms at 3000 and 2.2 s at 9000. A read of one table also grows with the catalog |
| `Table.Size` | NULL. `dbstat` scans the file, and the mattn driver is built without it. modernc has it |
| `Table.Rows`, `Owner`, `AccessMethod` | NULL. SQLite keeps no row count and no owner. The module of a virtual table is only in the text of its CREATE statement |
| `Index.Predicate` | the text after WHERE of a partial index, cut from the CREATE INDEX statement. Absent for an index that covers every row |
| `Index.Clustered` | true for the primary key index of a WITHOUT ROWID table, and false for every other index |
| `Index.Definition` | the CREATE INDEX statement. Absent for an index that SQLite made for a PRIMARY KEY or UNIQUE clause |
| `Index.Using`, `Persistence`, `ConstraintType` | `btree` (`diskann` on libSQL), `permanent`, and `p` or `u` for an index that SQLite made for a constraint |
| `Index.Size`, `Valid`, `Owner`, `Options` | NULL. There is no source |
| `Column.Storage`, `Compression`, `StatsTarget` | NULL. SQLite has none of the three |
| `Constraint.Enforced` | NULL. A key is checked only on a connection that turned `foreign_keys` on, so no catalog column says it |
| `Partitions`, `Policies`, `NotNulls`, `Inherits` | SQLite has none of the four |

A shadow table of an fts5 table is listed as a `table`. `pragma_table_list`
calls it `shadow`, and relabeling it is not done.

## Cassandra

Cassandra answers 17 of the 65, verified against 5.0.9 and 3.11.19.

It is the first database here that is not SQL, and CQL is narrower than the
name suggests. D62 holds the four consequences and this is the short version.

### The image is built here

The Apache image refuses three things this model has queries for, and its
entrypoint maps only eight `cassandra.yaml` keys to environment variables,
none of them these. So every release is rebuilt from
`test/cmd/dbrun/image/cassandra.Containerfile`, which `dbrun` embeds and builds when the image is missing, so
nothing has to be done first, and CI builds its own from the same file.

It turns on user defined functions, so `Functions` and `Aggregates` have
something to read and the fixture can create one. It turns on materialized
views, which 5.0 ships off and 3.11 ships on, so `Views` behaves the same on
every release. And it sets `PasswordAuthenticator` and `CassandraAuthorizer`,
without which `system_auth` holds one role and no grant, and D61 has no second
principal to compare against.

The key names changed and the build handles both. 3.11 and 4.0 write
`enable_user_defined_functions` and `enable_materialized_views`, and 4.1 and
later write `user_defined_functions_enabled` and `materialized_views_enabled`.
usql publishes an image that does the same thing, as
`docker.io/usql/cassandra`, and its Dockerfile edits only the older spelling,
so it has no effect on 5.0. The build here checks the result rather than
trusting sed, because a sed that matches nothing changes nothing and says so
to nobody.

The superuser is `cassandra` and so is its password. It is the one product
here that does not use the shared test password, because Cassandra creates
that pair and it is the only one that works until somebody changes it.

The setup also makes the ordinary user `dbmeta_user` on every release, with the
shared test password. It can log in, it is no superuser, and it holds no
permission. Every role reads `system` and `system_schema`, so it gets the same
answers as the administrator except for the tables that hold roles and
permissions. On Cassandra, `Roles`, `RoleGrants` and `Privileges` are refused
to it, and so is `Settings` from 4.0, which reads `system_views`. On ScyllaDB,
`Privileges`, `RoleGrants`, `RoleSettings`, `Roles` and `Settings` are refused.
`test/testdata/parity.txt` holds the answer under `[cassandra/same/user]` and
`[scylla/same/user]`. D195 has the reasoning.

### What it answers

Keyspaces as schemas, tables, columns, materialized views as views, user
defined types, indexes, index columns, the primary key as a constraint and its
columns, triggers, comments, functions, aggregates, roles, role grants,
privileges and settings.

`Settings` needs 4.0, where the `system_views` keyspace arrived. Everything
else answers on every release from 3.11 up. That is the only fragment gated on
a Cassandra release, which is why the tested pair spans it. The other
fragments gate on the `scylla` key.

### No statement filters, and the Keep function of each binding does

CQL has no `OR`, no `IS NULL` outside a materialized view definition, and a
partition key takes only `=` or `IN`. The form every other model uses,
`(@schema IS NULL OR col LIKE @schema)`, cannot be written. There is no
`NOT IN` either, so the keyspaces Cassandra keeps for itself cannot be
excluded in a statement.

So every statement returns every row, and each binding sets `Keep`
(D200). `Query.All` calls it after `Scan` and does not yield a row that it
rejects. `schema` matches the keyspace, `name` matches the name of the
object, and `parent` matches the table that a column, an index, a constraint
or a trigger belongs to. The match is `dbmeta.LikeFold`, so `%` is any run of
characters, `_` is one, and the case of letters does not matter, because an
unquoted CQL name is case insensitive (D202). `types` on Tables keeps `table`, which is the only type this query
returns. A role, a role grant, a role setting and a setting belong to no
keyspace, so a `schema` pattern other than empty or `%` matches nothing for
them.

`with_system` is false by default, as it is for every model. It hides the
keyspaces `system`, `system_schema`, `system_auth`, `system_distributed` and
`system_traces`, which Cassandra 3.11 and 5.0 list in `system_schema.keyspaces`.
ScyllaDB lists those and also `system_replicated_keys`, which both releases
have, `system_distributed_everywhere`, which 2025.1 has, and `audit`, which
2026.3 has. The model holds the list as data. The virtual keyspaces
`system_views` and `system_virtual_schema` are in the list too, but Cassandra
5.0 keeps them in `system_virtual_schema.keyspaces`, so no statement here
returns them. A privilege on a resource inside a system keyspace is hidden too.

The cost is that one statement reads every row of the catalog and the filter
runs in Go, so the work grows with the whole catalog and not with the rows
returned. D47 does not allow that for a model that can filter in SQL, and this
one cannot. The catalog of Cassandra is small. Measured on Cassandra 3.11 with
a scratch keyspace of 300 tables and 1200 columns beside the 36 tables and 234
columns of the system keyspaces: Tables read 336 rows in 3 ms and returned
300 of them, and Columns read 1434 rows in 9 ms without a filter and in 5 ms
with one, on a local container.

### A derived field is derived in Go

There is no `CASE` and no expression. `Column.Nullable` and
`Column.PrimaryKey` are both read from `system_schema.columns.kind`, which the
statement selects twice, and `Scan` turns each into its boolean. A column of
kind `partition_key` or `clustering` is in the primary key, and the primary key
is the only thing in Cassandra that cannot be null.

### No order

CQL orders rows within one partition and by a clustering column. A result that
spans partitions arrives in token order, so the same query can return the same
rows in another order on another cluster. No query writes `ORDER BY`, because
one does not make the answer ordered.

### What the fixture builds, and what it needs

`models/cassandra/fixture` creates the keyspace `dbmeta_fixture` with the core
objects D53 asks every fixture for, plus one of every Cassandra object the
queries read: a user defined type, a secondary index, a materialized view, a
function, an aggregate built on a second function, two roles, a grant between
them and a permission on the keyspace. Twenty one steps. Cassandra runs
nineteen on every release and skips the two service level steps, which run on
ScyllaDB alone.

It needs the image this repository builds. Against the published one the
function, the view and the roles are all refused, and the test says so rather
than quietly building less.

Three tables show three shapes of primary key, because that is the one
constraint Cassandra has and the queries report it: `author` has a partition
key alone, `book` has a partition key and a clustering column, and `region`
has a two column partition key.

No trigger. A Cassandra trigger names a Java class that has to be on the
server's classpath already, so the fixture creates none and `Triggers` returns
no rows anywhere. It is the one registered query with no fixture object.

### A null arrives as a null

The tests use `github.com/xo/cassandra`, which reports a CQL null as NULL. The
driver before it sent an empty string for every null, and D62 worked around
that by discarding each padded column. D93 records the change.

A real catalog column that is null now reaches the caller as NULL, so `Valid`
means what it means on every other dialect. Cassandra itself stores the empty
string rather than a null for a table with no comment, so an uncommented table
reports a valid empty comment. That is what the catalog holds.

### What the conformance test says

Cassandra is in `test/testdata/conformance.txt` under `[cassandra]`, and it
agrees with the relational databases on less than they agree with each other.
That is why `TestConformanceAgreementHolds` now measures twice: the relational
databases against the floor they have always held, and every database against
a smaller one. If Cassandra counts with the rest, the floor drops from 23
lines to 6 and is too low to notice a regression anywhere.

Two differences and neither is a fault. There is no `table recent view` line,
because `Tables` returns no view: a materialized view is in
`system_schema.views` and CQL has no UNION to put the two together. And the
column ordinal is a position within the primary key rather than within the
table, because the catalog keeps no declaration order and holds a table's
columns alphabetically.

### What Cassandra has none of

No sequence, no domain, no enumerated type, no cast, no collation catalog, no
operator, no text search object, no extension, no foreign data wrapper, no
tablespace and no large object. No `Databases` either: a keyspace is the top
of the tree and the cluster above it is in `system.local`, which no single
statement can reach from `system_schema` because CQL has no join.

`CurrentSchema` and `CurrentUser` are absent and both models agreed. There is
no CQL expression for either. `system_views.clients` from 4.0 lists every
connection with the user on it and cannot say which one is asking, so it is
not the same fact.

`RoutineParameters` is the one that is present and not returned.
`system_schema.functions` holds `argument_names` and `argument_types` as two
parallel lists on the function's own row, and turning them into one row per
parameter needs an unnest that CQL does not have. The types are in
`Functions.ArgTypes` as one text.

`ColumnStats` is absent from the catalog. The statistics Cassandra keeps are
per SSTable and per node, in `system_views` from 4.0, which is a different
thing from the per column distribution `psql` prints.

`PartitionedTables` is a stretch and is left unsupported under the D43 rule.
Every Cassandra table is partitioned, so a list of the partitioned ones is a
list of all of them and says nothing.

### The describe fields (D210)

The table options, the class and the options of a custom index, and `Prosrc` of a function are filled. The ScyllaDB section below lists them.

## ScyllaDB

ScyllaDB answers 18 of the 65, verified against 2025.1.15, 2026.1, 2026.2 and
2026.3.1. It is a second product that speaks CQL, and `models/cassandra` reads
it. Cassandra is the reference product and ScyllaDB is the flavor, the way
MariaDB and MySQL share `models/mysql`. D91 is the decision.

### How the model knows which product it has

ScyllaDB puts a `supported_features` column in `system.local`, and Cassandra
has no such column. The version query reads the whole row as one JSON text,
with `SELECT JSON *`, because CQL refuses a statement that names a column the
table does not have. A row with that column is ScyllaDB, and the model records
the `scylla` version key.

Its `release_version` is 3.0.8 on every release measured. That is the
Cassandra release that ScyllaDB keeps compatible with, not its own release,
and it stays the main version, because it describes the catalog that ScyllaDB
offers. The ScyllaDB release is in `system.versions`, which Cassandra does not have,
so a second statement reads it, and only on ScyllaDB. D92 is the decision. A
role granted nothing is refused `system.versions` and served `system.local`,
so it learns that it is talking to ScyllaDB and not which release. The
version read does not fail for it.

### What differs from Cassandra

ScyllaDB keeps `system_schema` as Cassandra 3.0 laid it out, so most queries
need nothing. Four things differ, and each one is a fragment on the `scylla`
key:

1. Roles, role grants and permissions are in `system.roles`,
   `system.role_members` and `system.role_permissions`, with the columns that
   Cassandra has in `system_auth`. `system_auth.roles` is refused as an
   unconfigured table on both 2025.1 and 2026.3.
2. Settings are in `system.config`, which also records a type for each
   setting and where its value came from. The value arrives as JSON, so a text
   value is in double quotes. The `system_views` gate at Cassandra 4.0 does
   not reach ScyllaDB, because its main version is 3.0.8, so the ScyllaDB
   fragment is the one that answers.
3. No literal is allowed in a select list. 2025.1 refuses `(text)NULL`,
   `(boolean)false` and `CAST(false AS boolean)` as syntax errors. 2026.3
   takes the cast and still refuses every NULL. So where the Cassandra
   statement selects a literal, the ScyllaDB fragment selects a real column of
   the same table under the same name, and Scan discards that column on both
   products and sets the known value itself.
4. `RoleSettings` has a source. `system.role_attributes` holds a value set on
   a role, and `ATTACH SERVICE LEVEL` is what sets one. It gives the role's
   sessions the timeout and the share of the server that the service level
   names, which is the ScyllaDB form of `ALTER ROLE ... SET`. The row is per
   attribute, because CQL cannot group.

### A secondary index is also a view

ScyllaDB builds a secondary index as a materialized view. `Views` returns the
view that backs the fixture's index, `book_author_index`, beside the view the
fixture creates, so it reports two rows where Cassandra reports one. The
catalog holds the row, and hard rule 13 says to return it. A consumer can tell
the two apart with `Indexes`, which names the index.

### The container needs its settings as arguments

The published image `docker.io/scylladb/scylla` passes every argument that its
entrypoint does not know to `scylla` itself, so nothing is built. The
arguments turn on `PasswordAuthenticator`, `CassandraAuthorizer` and user
defined functions, and hold the server to one shard and one gigabyte.

2026.3 does not create the `cassandra` role that Cassandra creates. So the
superuser is named at startup with `--auth-superuser-name` and a salted
password, which 2025.1 also takes. The pair is `cassandra` and `cassandra`,
the same as the reference product, so one set of test helpers logs in to both.
The entrypoint writes the arguments into a file that a shell reads, so each
dollar sign in the salted password is escaped once.

### What the fixture does differently

Three steps have a ScyllaDB form, and two steps run on ScyllaDB alone:

1. The keyspace uses `NetworkTopologyStrategy` and turns tablets off.
   ScyllaDB refuses `SimpleStrategy`, because it places a new keyspace on
   tablets, and 2025.1 refuses a secondary index and a materialized view on a
   keyspace that uses tablets.
2. The two functions are written in Lua. ScyllaDB runs no Java, and it takes
   Lua and WebAssembly behind an experimental flag.
3. A service level is created and attached to `dbmeta_reader`, so that
   `RoleSettings` has a row. The teardown drops it last.

Twenty one steps run on ScyllaDB and none is skipped. Cassandra runs nineteen
and skips the two service level steps.

### What a second opinion found

Gemini and DeepSeek were both asked to sort the 38 unanswered kinds. Both
called 35 of them absent and named three leads. None of the three is a
source:

| Lead | What the server says |
| --- | --- |
| `Databases` from `system_schema.keyspaces` | a keyspace is already what `Schemas` returns, and one object cannot be both |
| `Languages` from `SELECT DISTINCT language FROM system_schema.functions`, from DeepSeek | refused: `SELECT DISTINCT` works only on partition key columns |
| `RoutineParameters` from `argument_names` and `argument_types` | they are two lists on the function's own row, and CQL cannot unnest a list into one row per parameter, the same as on Cassandra |

`RoleSettings` from `system.role_attributes` was found here rather than
offered, while reading the tables that the system keyspace holds.

### What the conformance test says

ScyllaDB gives the canonical answer that Cassandra gives, line for line, under
`[cassandra]` in `test/testdata/conformance.txt`. The flavor agrees with the
reference product on everything the projection reads.

### Which answers depend on who is asking

A role granted every permission on the fixture keyspace is refused the same
four queries as on Cassandra, from the `system` tables rather than from
`system_auth` and `system_views`, and is refused `RoleSettings` as well. All
four releases give the same answer, under `[scylla/same/grantee]` in
`test/testdata/parity.txt`.

### The describe fields (D210)

Tables reads the table options from `compaction`, `compression`, `gc_grace_seconds` and `default_time_to_live` of the same row, as `compaction=..., compression=..., gc_grace_seconds=..., default_time_to_live=...`. Indexes reads the class and the other options of a custom index, which the storage attached index of Cassandra 5.0 has. Functions reads `body` as `Prosrc`. The size and the rows are NULL, because CQL has no join to `system_views.disk_usage` or `system.size_estimates`. The owner, the persistence and the column fields have no source. ScyllaDB fills the same fields, and has no custom index.

## ClickHouse

ClickHouse answers 25 of the 65, verified against 26.9.2.8 and 25.8.33.6.

### system, not information_schema

ClickHouse ships both. `information_schema` is an emulation that reports what
the standard names and drops everything that makes a ClickHouse table what it
is: the engine, the partition key, the sorting key, the compression codec per
column, and the data skipping indices. Every query here reads `system`, which
is about 150 tables and the richest native catalog in this project after
PostgreSQL's.

### A database is a schema

ClickHouse has one level of namespace and calls it a database, so `Schemas`
and `Databases` read the same table under both names. `models/mysql` does the
same thing with `information_schema.SCHEMATA` for the same reason.

### What it answers

Schemas and databases, tables, columns, views, indexes, index columns,
constraints, comments, partitioned tables, types, collations, settings, roles,
role grants, privileges, functions, aggregates, tablespaces, foreign servers,
foreign tables, the current schema and the current user.

Two of those are an analogue rather than the same object, and both reviews
agreed they are real rather than a stretch. A storage policy names the volumes
and disks a table's parts live on, which is what a tablespace is for. A table
whose engine is MySQL, PostgreSQL, S3 or one of the others reads rows another
system holds, which is what a foreign table is, and a named collection is the
stored connection it uses.

### Two columns arrived in the same release

`system.constraints` and `system.functions.deterministic` are both absent on
25.3, 25.8 and 26.1, and both present on 26.8 and 26.9. They gate at 26.8,
which is the release they were first seen in rather than the release they
arrived in: somewhere in 26.2 to 26.8 is as precise as a check against running
servers can be, and gating late under reports rather than breaking.

`Constraints` is the whole query, so an older server is told it is too old.
Volatility is one column of `Functions`, so an older server gets it padded and
`Field.Min` says which is which.

### What Cassandra taught us to check here too

The conformance projection contributes no constraint lines for ClickHouse.
There is no primary key constraint, no foreign key and no unique constraint,
and `system.constraints` holds the expression a CHECK asserts rather than the
columns behind it, so the model registers `Constraints` and not
`ConstraintColumns`. If ClickHouse counts in the cross family agreement
number, the number drops from 23 lines to 14. So ClickHouse is measured
separately for a stated reason, the way Cassandra is.

### What the fixture cannot build

A named collection. Creating one needs
`access_control_improvements.named_collection_control` in the server
configuration, and the official image exposes no variable for it. The
`ForeignServers` query is verified to run and returns no rows, which is the
same position Cassandra's triggers are in.

Everything else the fixture builds, including a user, a role and a grant,
which need `CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT` and `container/clickhouse.go`
sets it.

### What ClickHouse has none of

No sequence, no domain, no trigger, no operator catalog, no extension, no
large object, no text search object of the shape psql names, and no user
defined type: Enum, Array and Tuple are spelled inside a column's type rather
than declared.

To answer `EnumValues`, a query must parse the enum out of a type string,
which is not a catalog read. `RoutineParameters` is absent because a ClickHouse function is
overloaded across types and `system.functions` records no signature.
`ColumnStats` is absent: `system.columns` carries compressed and uncompressed
sizes and nothing about distribution, and what `system.parts` holds is per
part rather than per column.

### The describe fields (D208)

| Field or kind | What ClickHouse fills |
| --- | --- |
| `Table.Persistence` | `temporary` for a temporary table, and `permanent` for the rest, which includes a Memory table. NULL for a view |
| `Table.AccessMethod` | the engine |
| `Table.Size`, `Table.Rows` | `total_bytes` and `total_rows` of `system.tables`: the compressed bytes and the rows of the active parts. NULL for an engine that keeps no number, such as a view |
| `Table.Options` | `partition_by`, `order_by`, `primary_key`, `sample_by` and `storage_policy`. NULL when none is set. The SETTINGS clause is only in `engine_full`, as text |
| `Table.Owner`, `RowSecurity` | NULL. `definer` of a view is not an owner, and `system.row_policies` is closed to a user who has no grant on it |
| `Column.Compression` | the codec chain of `compression_codec`, such as `CODEC(ZSTD(3))`. NULL when the column uses the default |
| `Column.Storage`, `StatsTarget` | NULL. `system.columns.statistics` names the kinds of statistics, and ClickHouse has no target |
| `Index.Size`, `Options`, `Using` | the compressed data and the marks of the skipping index, `granularity=N`, and `type_full` |
| `Constraint.Enforced` | true for CHECK, which the server tests on insert, and false for ASSUME, which it never tests. It needs 26.8, as the rest of `constraints` does |
| `Function.Prosrc` | NULL. `create_query` is the whole statement |
| `PartitionedTable.AccessMethod`, `DirectSize`, `TotalSize` | the engine, and `total_bytes` twice, because a partition is not a table |
| `Partitions` | new. One row for each partition id of each table that has a PARTITION BY, from `system.parts`. `Bound` is the value of the partition expression. The rows of a partition have no field. An ordinary user is refused `system.parts` |
| `Policies` | new. One row for each row policy, from `system.row_policies`. The command is always select, and `Roles` is NULL for TO ALL. `Enabled` is NULL: `system.row_policies` has no column for a switch on 25.8 or 26.9, and a row policy has no DISABLE (D211). An ordinary user is refused the table |
| `NotNulls`, `Inherits` | none. Nullable is part of the type, and ClickHouse has no inheritance |

Gemini agreed that ClickHouse has no owner, no statistics target and no flag for
row security. DeepSeek ran out of tokens on every question.

## Trino

Trino is a query engine rather than a store. It reads other people's data
through a connector and keeps almost nothing of its own, so most of what it
cannot answer is missing because the thing does not exist rather than because
the catalog hides it.

### A catalog is a real level, and only Trino and Presto have one

Every other model here returns an empty catalog or repeats the database name
into it, because the products have two levels of namespace and `psql` has
three. Trino has all three. A table is `catalog.schema.name`, a catalog is a
configured connector, and one server reaches many at once.

So Trino and Presto are the only models that answer a catalog filter, and
[`dbmeta.Args`](../filter.go) has carried the field all along waiting for them.

### system.jdbc, not information_schema

Trino ships an `information_schema` inside every catalog and a `system`
catalog beside them, and the two differ in reach. A query against
`memory.information_schema.tables` sees the memory catalog and nothing else,
and the catalog cannot come from a bind parameter, so a filter that names a second
catalog returns nothing rather than an answer. That is a wrong answer
rather than an empty one, which rule 13 does not allow. The tables under
`system.jdbc` span every catalog the server has.

`system.jdbc` is also the richer of the two. Its `columns` table carries the
column comment in `remarks`, and `information_schema.columns` has no column
for a comment at all.

### What it answers

13 of the 65. Catalogs as databases, schemas, tables, columns, views,
comments, types, access methods, roles, role grants, privileges, the current
schema and the current user.

A connector is the analogue for an access method, the same way a storage
engine is for MariaDB: both answer "how is this stored and reached". The
`comment` on a connector is the list of catalogs it backs, because one
connector can back several.

### The one query that cannot span catalogs

Views. No cross catalog source carries a view definition:
`system.metadata.materialized_views` has one and covers materialized views
only, and a plain view's definition lives in the `information_schema.views` of
its own catalog. A read of every catalog takes one statement per catalog,
which rule 13 forbids.

So Views reads the session catalog, and its `schema` parameter says so where a
caller reads it. Every other query here spans catalogs.

### A boolean is not an integer

Every other model writes `@with_system = 1` and the server coerces. Trino
applies no implicit conversion between a boolean and an integer and refuses
the statement with `Cannot apply operator: boolean = integer`, so this model
writes `= true`. Seven queries were written the other way first and all seven
failed at once, which is the cheapest way for that to be found.

### What the fixture cannot build

No primary key, no foreign key, no unique constraint and no check. Trino has
no constraint of any kind at any release, so nothing enforces that a book has
an author. No sequence, no trigger, no index and no user defined type. No
default: Trino parses `DEFAULT` and the memory connector keeps none.

No role and no grant either, and that one is the connector rather than the
engine. A Trino role belongs to a catalog, only a connector that implements
role management has any, and the memory connector answers "does not support
role management" to `CREATE ROLE`. Roles, RoleGrants and Privileges are
verified to run and return no rows, and `TestTrinoRolesAreEmptyOnMemory` pins
that so it stays distinguishable from a broken query. A connector with
sql-standard security, such as Hive, populates all three.

No materialized view: the memory connector refuses to create one.

### What the conformance test says

Trino builds every core object D53 asks for, including the view, and the
column ordinals and nullability match every other database. It is left out of
the relational agreement count for the same reason ClickHouse is: with no
constraint catalog every column reads `primary_key=false` where the others
agree on the key, and there are no constraint lines to compare.

### What a second opinion found, and what it cost to check

D43 requires asking at least two models about the queries a new dialect cannot
answer. Gemini and DeepSeek were both asked to sort 42 unanswered kinds into
absent, present under another name, and derivable from one statement.

Every lead was run against a real server, which is the part of the rule that
matters. DeepSeek named eight sources that do not exist:

```
system.metadata.functions             ABSENT
system.metadata.partitions            ABSENT
system.metadata.types                 ABSENT
system.metadata.column_comments       ABSENT
system.metadata.session_properties    ABSENT
information_schema.parameters         ABSENT
information_schema.table_constraints  ABSENT
information_schema.key_column_usage   ABSENT
```

Three of its answers rested on `system.metadata.functions` alone: functions,
aggregates and operators. `SHOW CASTS` is not a statement Trino has, and no
table reports `table_type = 'FOREIGN'`. Gemini named nothing that does not
exist, and put all but six kinds in absent.

Two answers were worth the exercise.

Functions cannot be read as a relation, and now that is settled. Gemini
said `SHOW FUNCTIONS` cannot be wrapped in a subquery and it is right, though
not for the reason it gave. The parser does not reject it: it reads `SHOW` as
a table name and reports `Table 'memory.default.show' does not exist`. So
there is no table valued source for the function list, the column names carry
spaces, and Functions and Aggregates stay unanswered. `SHOW SESSION` has the
same shape, which is why Settings is unanswered too, and both models agreed
`SHOW STATS FOR` has no table valued form, so ColumnStats is as well.

information_schema.columns has an undocumented column. Gemini derived
partitioned tables from `extra_info = 'partition key'`. `SHOW COLUMNS` does
not list `extra_info` and the column resolves anyway, which a control
settled: a name that really does not exist fails with `Column
'definitely_not_a_column' cannot be resolved`, and `extra_info` returns 0 non
null values over 34 rows. It is real, and the memory connector never sets it.
A connector that partitions, such as Hive, does.

PartitionedTables is left unanswered on that basis rather than on absence. The
source exists and no connector in the test image populates it, so rule 9 has
no object to build and nothing to verify the query against.

### Which answers depend on who is asking

None of them, and that is the measurement rather than a gap in it. Trino has
no users to create: a client states a principal on every request and the
server takes it, because the image configures no authenticator. With no access
control plugin the server then allows that principal everything, so the only
query that answers differently for a second principal is `current_user`, which
is the one that is supposed to.

### The describe fields (D210)

None are filled. `system.metadata.tables_authorization` is empty for the memory connector, `SHOW STATS` is one statement for each table, and the memory connector has no `$partitions` table.

## Firebird

`models/firebird` answers 25 of the 65, against Firebird 3.0.14, 4.0.7 and
5.0.4.

### It reads RDB$, and there is no information_schema

Firebird has no `information_schema` at any release in range. Its catalog is
the `RDB$` tables, which are ordinary tables inside the database that a
statement reads the way it reads any other. Two other prefixes matter. `MON$`
is the monitoring set, and `MON$DATABASE` is where the attached database
describes itself. `SEC$` is a view on the server's security database, and it
is where users live, because a user belongs to the server and a role belongs
to the database.

Two properties of `RDB$` shape every query here, and both cost a round of
wrong answers before they were understood.

Every name is `CHAR(63)` and blank padded. Equality pads and `LIKE` does not,
so `RDB$RELATION_NAME LIKE 'AUTHOR'` matches nothing at all: the stored value
is AUTHOR followed by 57 spaces. A filter written without a trim looks correct
and returns an empty result rather than an error. Only the trailing blanks are
padding, so the model trims those alone and not a leading space, which a
quoted identifier is allowed to have.

Every bind parameter needs a cast. Firebird takes the type of a bare parameter
from what it is compared with, so `? = ''` types it `VARCHAR(0)` and the server
then refuses any value:

	arithmetic exception, numeric overflow, or string truncation
	string right truncation
	expected length 0, actual 6

An empty filter still passes, which is what makes it worth writing down. A
first run with no arguments reports nothing wrong.

### There are no schemas, and none is invented

Firebird 3.0 through 5.0 has no schemas. Every object lives in one namespace
and names are unique across the database. Firebird 6.0 adds SQL schemas and is
out of range.

So `Schemas` and `CurrentSchema` report `NotSupported`, and every other query
returns an empty schema. A caller that cannot see the server is not able to
tell an invented name from a real one, and an empty result must never
stand in for `NotSupported`, which is D34. `TestFirebirdSchemasAreNotSupported`
holds both halves of that.

A Firebird database is a file rather than a name, and the server keeps no
catalog of the files it has served, so `Databases` returns exactly one row: the
database attached, named by its path.

### What it answers

Tables, columns, views, indexes, index columns, constraints, constraint
columns, triggers, event triggers, sequences, domains, functions, routine
parameters, types, collations, roles, role grants, privileges, comments,
databases, settings, publications, publication tables and the current user.

Three of those need 4.0 and report `TooOld` on 3.0, which D63 added the state
for: `Settings` reads `RDB$CONFIG`, and `Publications` and `PublicationTables`
read `RDB$PUBLICATIONS` and `RDB$PUBLICATION_TABLES`. None of the three exists
before 4.0.

Two answers take a second source and are worth naming.

A check constraint has no index, so it has no entry in `RDB$INDEX_SEGMENTS`
and the join every other constraint uses cannot reach its columns. Firebird
implements a check with a pair of system triggers and records the columns as
those triggers' rows in `RDB$DEPENDENCIES`, and nowhere else. That is the
second arm of `ConstraintColumns`, and `TestFirebirdCheckConstraintColumns`
exists so that it cannot quietly stop working: without it the result is
a shorter list.

A foreign key names the unique constraint it references rather than the table,
so reaching the target column takes three more joins: to `RDB$REF_CONSTRAINTS`
for the referenced constraint, to its row in `RDB$RELATION_CONSTRAINTS` for the
table, and to that constraint's index segments for the column in the matching
position.

`Roles` is one statement over two places, because Firebird splits what
PostgreSQL keeps in one. `SEC$USERS` holds the users and `RDB$ROLES` holds the
roles, and `can_login` is what tells them apart.

A member of a package is left out of `Functions`, the way `models/oracle`
leaves one out. It is not callable by name on its own, and the package is what
a caller names.

### What it cannot answer, and why

Thirty-two kinds have no answer and every one of them is absent from the
product rather than hidden by the catalog.

There are no schemas, no tablespaces, no user defined casts, no operators, no
user defined aggregates, no enumerated types, no extensions, no foreign tables
or foreign data wrappers, no text search objects, no operator classes or
families, no partitioned tables, no extended statistics, no default privileges
and no per role settings. Firebird has one index kind and no pluggable access
method, so there is nothing for `AccessMethods` to list.

Three deserve a reason rather than a word.

`LargeObjects` has no answer because a Firebird BLOB is addressed from the row
that holds it and page chain that follows it. There is no catalog of them to
list, so this is absence rather than reach.

`ColumnStats` has no answer either, and the near miss is worth recording.
`RDB$INDEX_SEGMENTS.RDB$STATISTICS` holds a selectivity, but it is an index
prefix selectivity rather than a per column statistic, and it is none of the
things `ColumnStat` carries: no average width, no null fraction, no distinct
count, no most common values. A report of it gives a different number under
the same name.

`Languages` is the one analogue left unsupported as a stretch, which rule 14
asks for explicitly. `RDB$FUNCTIONS.RDB$ENGINE_NAME` and the same column on
`RDB$PROCEDURES` name the external engine a routine is written for, so the
engines actually in use are derivable in one statement. That is a list of
languages in use and not a catalog of languages installed, and Firebird has no
catalog of the second. That list misses an unused engine, and a caller cannot
tell. The fact is not lost: `Function.Language` carries it per routine, which
is where Firebird records it.

`Subscriptions` is absent for a reason that is not obvious from the name.
Firebird 4.0 has logical replication and the publisher side is in the catalog,
which is why `Publications` answers. The subscriber side is configured in
`replication.conf` on disk and never reaches the database, so there is nothing
to read.

Firebird 5.0 added a partial index, whose predicate is in
`RDB$INDICES.RDB$CONDITION_SOURCE`. `dbmeta.Index` has no field to carry a
predicate, so the model does not read it and nothing here gates on 5.0. Adding
the field is a D47 question for every model rather than for this one.

### What a second opinion found

D43 and hard rule 14 require asking at least two models about the kinds a
first pass cannot answer, and this is the second time the rule has paid by
catching an invention rather than by finding a source. Trino was the first.

Gemini was asked about twelve concepts and answered absent for all twelve, and
every one of those answers was right. It also correctly identified
`RDB$INDEX_SEGMENTS.RDB$STATISTICS` as an index prefix selectivity rather than
a column statistic.

DeepSeek named seven sources. Six of them do not exist on a real Firebird 5.0
server and the seventh exists and is never filled:

| Named | What the server says |
| --- | --- |
| `RDB$INDEX_TYPES` | no such table |
| `RDB$INDICES.RDB$INDEX_TYPE_NAME` | no such column |
| `RDB$RELATION_FIELDS.RDB$STATISTICS` | no such column |
| `RDB$SUBSCRIPTIONS` | no such table |
| `RDB$SUBSCRIPTION_TABLES` | no such table |
| `RDB$FTS_CONFIG` | no such table |
| `RDB$FTS_STOPWORDS` | no such table |
| `RDB$FUNCTIONS.RDB$FUNCTION_TYPE = 2` for aggregates | the column exists and is NULL on every row |

Each was run rather than read, which is the whole of the rule. A lead is a lead
and nothing more.

### What the fixture cannot build

Nothing. The Firebird fixture builds all six core objects D53 asks for and all
33 of its steps run on 3.0, 4.0 and 5.0, except the one publication step, which
carries a gate and is skipped on 3.0 for the same reason `Publications` reports
`TooOld` there.

It creates no user, and that is deliberate rather than a gap. A Firebird user
lives in the server's security database, which every database on that server
shares, so a user that a fixture creates changes a database the test never
opened. `test/parity_test.go` creates its principal and removes it again,
because a parity test has to.

### What the conformance test says

Firebird's section of `test/testdata/conformance.txt` is identical to
PostgreSQL's except for three lines, and on all three Firebird agrees with the
other eight databases rather than differing from them. PostgreSQL reports
`has_default=true` on `author_id`, `book_id` and `shipment_id` because its
fixture declares them `serial`. Firebird's keys are plain integers, the way
they are everywhere else.

Everything else matches exactly: the tables and the view, every column with its
ordinal, nullability and primary key flag, the composite primary key on
`region`, the composite foreign key from `shipment` into it with both target
columns, the unique constraint and the check.

### Two faults in the driver, and where they do and do not matter

Neither affects a consumer, and both cost enough to find that they are written
down here.

`nakagami/firebirdsql` returns a stale error for user management. After one
such statement fails, every later one on the same connection reports the first
error, while an ordinary query on that connection still works:

	DROP USER dbmeta_absent  -> record not found for user: DBMETA_ABSENT
	CREATE USER dbmeta_p1    -> record not found for user: DBMETA_ABSENT
	SELECT COUNT(*) ...      -> <nil>
	CREATE USER dbmeta_p2    -> record not found for user: DBMETA_ABSENT

Worse, a successful `CREATE USER` poisons a later read of `SEC$USERS` on the
same connection. The server answers EOF and drops the attachment, and the
pool's next connection then fails its handshake, so every query after that
reports a protocol error. It takes a few statements in between to become
reliable, which is why it looked intermittent until it was pinned down:

	CREATE USER ...          -> <nil>
	... ten metadata queries -> <nil>
	SELECT FROM SEC$USERS    -> EOF

`dbmeta` issues no user management statement at any time and never will, so
neither fault can reach a consumer and `Roles` keeps `SEC$USERS`. The parity
test creates a principal, so it runs every such statement on a connection of
its own and closes it. That was measured against Firebird 5.0.4 with
`nakagami/firebirdsql` v0.9.21.

### The describe fields (D209)

D198 to D203 gave the root types fields that only PostgreSQL filled. D209 filled
what Firebird has a source for, on 3.0, 4.0 and 5.0. Every statement still
returns the same columns on every release.

| Field | Firebird |
| --- | --- |
| `Table.Owner` | `RDB$OWNER_NAME` |
| `Table.Persistence` | temporary for a global temporary table, NULL for a view, permanent for the rest |
| `Table.Options` | `on_commit` for a global temporary table, `external_file` for an external table, and `sql_security` from 4.0 |
| `Table.AccessMethod`, `Size`, `Rows` | NULL. Firebird has one engine, keeps no size and keeps no row count. `MON$` shows the pages of the open attachments only |
| `Index.Owner`, `Persistence`, `Size`, `Clustered`, `ReplicaIdentity`, `Options`, `Definition`, `Using` | NULL. There is no source, or the fact is the one `Type` already holds |
| `Index.Valid` | `RDB$INDEX_INACTIVE`, so an index set INACTIVE is false |
| `Index.Predicate` | `RDB$CONDITION_SOURCE` without the WHERE word, from 5.0. NULL below that, which has no partial index |
| `Index.Deferrable`, `InitiallyDeferred` | from the constraint that owns the index, which is always false. NULL for an index no constraint owns |
| `Index.ConstraintType` | p, u or f from the owning constraint. A Firebird foreign key has an index of its own, so f is an addition to PostgreSQL's p, u and x |
| `Column.Storage`, `Compression`, `StatsTarget` | NULL. Firebird records no such choice |
| `Constraint.Enforced` | true. Firebird cannot disable a constraint, so every one is enforced, though no catalog column says so (D212) |
| `Function.Prosrc` | the same text as `Source`, from `RDB$FUNCTION_SOURCE` and `RDB$PROCEDURE_SOURCE` |
| `NotNulls` | new. `RDB$RELATION_CONSTRAINTS` of the type NOT NULL, with the column from `RDB$CHECK_CONSTRAINTS` |
| `Partitions`, `Policies`, `Inherits` | Firebird has none of the three |

## SAP HANA

`models/hana` answers 33 of the 65, against SAP HANA 2.00.088, which is
Tested, and 2.00.076 and 2.00.082, which run nightly. It ties SQL Server for
the richest answer after PostgreSQL and CockroachDB, which shares the
PostgreSQL model.

### It reads SYS, and there is a lot of it

SAP HANA has no `information_schema`. Its catalog is the SYS schema, a large
set of views that a statement reads like any other. That is why this model
answers as much as it does: a concept PostgreSQL has usually has a view here,
rather than an analogue that has to be argued for.

Three prefixes appear. A plain name such as `SYS.TABLES` is the catalog. A
name beginning `M_` is a monitoring view and reports what the server is doing
now rather than what is defined. A name beginning `_SYS_` is a schema the
server owns and not a view, which is why the system filter tests a prefix with
`LEFT` rather than with `LIKE`: an underscore is a single character wildcard
in `LIKE`, so `LIKE '_SYS%'` also matches `ASYS` and anything else of that
shape.

Two things about writing SQL for it, both measured rather than read.

A flag is the string TRUE or FALSE, and HANA refuses a bare comparison in a
select list. `SELECT IS_PRIMARY_KEY = 'TRUE' FROM SYS.CONSTRAINTS` is a syntax
error rather than a boolean, and so is comparing a CASE to FALSE, so the model
has both a `yes` and a `no` helper and writes the inversion out.

A bind parameter needs no cast, which is the opposite of Firebird. HANA infers
the type from the comparison, including where the same parameter appears twice.

The catalog keeps changing for about a minute after the server first answers.
Measured on 2.00.088 on 2026-09-30, the statistics service made more than 100
views in `_SYS_STATISTICS` in that minute, recorded its installation as done
in `_SYS_STATISTICS.STATISTICS_PROPERTIES`, and made the views of
`_SYS_TELEMETRY` about 12 seconds later. Nothing changed after that. A tier run
once read the tables while this happened and counted 896 views and then 895,
so checkTypes in `test/scan_test.go` reads again once when two counts
disagree, and fails only when the disagreement stays.

### What it answers

Tables, schemas, columns, views, indexes, index columns, constraints,
constraint columns, triggers, sequences, partitioned tables, functions,
routine parameters, types, collations, roles, role grants, privileges,
comments, databases, settings, access methods, column statistics, extended
statistics, foreign data wrappers, foreign servers, user mappings, foreign
tables, subscriptions, text search configurations, the current schema and the
current user.

Four of those are the reason HANA is worth having. Smart data access is real
foreign data with a view per concept, so all four answer from a catalog rather
than by analogy:

| Kind | View |
| --- | --- |
| `ForeignDataWrappers` | `SYS.ADAPTERS` |
| `ForeignServers` | `SYS.REMOTE_SOURCES` |
| `UserMappings` | `SYS.REMOTE_USERS` |
| `ForeignTables` | `SYS.VIRTUAL_TABLES` |

Only Exasol, CockroachDB and PostgreSQL answer all four as well.
`SYS.REMOTE_SUBSCRIPTIONS` answers
`Subscriptions` as well, which is the subscriber half of replication.

Three more are worth naming. `SYS.DATA_STATISTICS` answers extended
statistics, `SYS.PARTITIONED_TABLES` answers partitioned tables, and
`SYS.TEXT_CONFIGURATIONS` answers text search configurations, which is the one
member of that family HANA has.

Two answers needed a decision rather than a view.

`AccessMethods` reports the row store and the column store. A HANA table is
held one way or the other and the choice is per table, which is the same
question a MySQL storage engine and a Trino connector answer. HANA keeps no
catalog of the kinds, so the query counts the tables that name each one, and
a kind nothing uses does not appear. `Tables` carries the same fact per table,
so the type is row table or column table rather than table.

`Constraints` derives the kind from two flags and the check text, because
`SYS.CONSTRAINTS` has no type column: a primary key sets both flags, a unique
key sets one, and a check sets neither and carries the condition. A foreign
key is not in that view at all and comes from
`SYS.REFERENTIAL_CONSTRAINTS`, which is the one place in this project where
the referenced column is on the row itself rather than reached through the
referenced constraint.

### What it cannot answer

24 kinds, and every one because HANA has no such object.

There is no `CREATE DOMAIN`, no enumerated type, no user defined cast,
operator or aggregate, no tablespace, no DDL or event trigger, no publication,
no default privilege and no per role setting. There is no catalog of
procedural languages either: `PROCEDURE_TYPE` and `FUNCTION_TYPE` say what one
routine is written in, which `Function.Language` carries, and there is no list
of the languages installed. That is the same stretch Firebird's engine names
are, and it is left unsupported for the same reason.

Two are worth a sentence rather than a word.

`LargeObjects` has no answer because a HANA LOB is addressed from the row that
holds it. There is no catalog of them.

`Publications` has no answer although `Subscriptions` does, and that is not an
oversight. HANA replicates by subscribing to a remote source, so the
subscriber half is in the catalog and there is no publisher object at all.

### What a second opinion found

Gemini was asked about twelve concepts. It answered absent for eleven and
named `SYS.REMOTE_SUBSCRIPTIONS` for the twelfth, which exists and is the
source `Subscriptions` now reads. That is the first time in three dialects
that a second opinion found a source rather than only confirming an absence.

DeepSeek named three and two do not exist:

| Named | What the server says |
| --- | --- |
| `SYS.TABLESPACES` | no such view |
| `SYS.PROCEDURAL_LANGUAGES` | no such view |
| `SYS.FUNCTIONS.FUNCTION_TYPE = 2` for aggregates | the column exists and holds BUILTIN or SQLSCRIPT2, never a number |

That is the third dialect running where DeepSeek invented a source and running
it was the only way to tell. See D43.

### What the fixture cannot build

Four things, and each is verified to run and return nothing rather than left
untested.

No remote source, virtual table or remote subscription: federating needs a
second database to federate to.

No text configuration: it is a repository object created outside SQL.

No data statistics object: `CREATE STATISTICS` needs rows, and the fixture
inserts none.

No column level grant, and this one is a product fact rather than a fixture
limit. `SYS.GRANTED_PRIVILEGES` carries a `COLUMN_NAME` column and HANA 2.0
SPS 08 has no `GRANT` syntax that fills it. Every spelling is a syntax error:

	GRANT UPDATE(rating) ON t TO r        -> syntax error near "("
	GRANT SELECT (rating) ON t TO r       -> syntax error near "("
	GRANT UPDATE ON t(rating) TO r        -> syntax error near "("

So `Privilege.ColumnAccess` is always empty. The query keeps the column
because the catalog has it, and `TestHANAHasNoColumnGrant` asserts both halves
so that the absence stays a decision.

No user either. A HANA user belongs to the tenant database and outlives the
schema, so `test/parity_test.go` creates its principal and drops it again.

### What the conformance test says

HANA's section differs from PostgreSQL's in three places and each one is a
real difference rather than a gap.

The three `has_default=true` lines are PostgreSQL's `serial` keys, where HANA
agrees with the other nine databases.

A view's columns are reported NOT NULL where PostgreSQL reports them nullable.
HANA computes a view column's nullability from the column behind it, and
`book_id` and `title` are both NOT NULL, so the view says so. PostgreSQL
reports a view column nullable whatever is behind it.

There is no `constraint book check (title)` line, and the reason is
structural. A HANA check belongs to the table rather than to a column:
`SYS.CONSTRAINTS` leaves `COLUMN_NAME` and `POSITION` NULL on a check row, so
`ConstraintColumns` has nothing to report and the conformance line is built
from that query. The check itself is in `Constraints` with its condition, and
`TestHANAConstraints` reads it. Firebird reaches a check's columns through the
dependencies of the triggers that implement it, and HANA implements a check
without a trigger, so there is no equivalent to follow.

### Which answers depend on who is asking

Ten queries answer differently for a user that is not the administrator,
which is the most of any product here. That is HANA rather than the model:
almost every SYS view filters itself by what the reader is allowed to see, so a grantee
sees fewer collations, fewer databases, fewer adapters and fewer settings, as
well as fewer roles and grants.

Three of the ten return the same number of rows with different values:
functions, triggers and views. Those carry a definition, and HANA returns the
row and withholds the text from a reader without the privilege. That is worth
knowing before a consumer treats a definition as always present.

Sequences were recorded as a fourth until 2026-09-29, and that was wrong. The
catalog keeps a sequence's numbers as DECIMAL, and the driver hands a DECIMAL
over as a value that holds a pointer, so its text differed on every read.
The query casts the numbers to BIGINT now, and both principals read the same
rows.

### The describe fields (D209)

D198 to D203 gave the root types fields that only PostgreSQL filled. D209 filled
what HANA has a source for, on 2.00.088 as the administrator, and for a user
with one grant and a user with none.

| Field | SAP HANA |
| --- | --- |
| `Table.Owner` | `OWNER_NAME` of `SYS.OWNERSHIP`. The schema owner is a different fact |
| `Table.Persistence` | unlogged for `IS_LOGGED` false, which is a table created NO LOGGING and is a temporary table too. Then temporary for a global temporary table, and permanent. NULL for a view |
| `Table.AccessMethod` | row or column, from `TABLE_TYPE` |
| `Table.Options` | `on_commit` for a global temporary table, `insert_only`, `auto_merge=off` and a `load_unit` that is not the default |
| `Table.Size`, `Rows` | NULL. `SYS.M_TABLES` has `TABLE_SIZE` and `RECORD_COUNT`, and any user reads it for the tables it can see, but a join to it scans every table. See D209 |
| `Index.Owner` | `OWNER_NAME` of `SYS.OWNERSHIP` |
| `Index.Using` | the index kind, lower cased. It is the same text as `Type` |
| `Index.ConstraintType` | p from `CONSTRAINT`, and u for an index that `SYS.CONSTRAINTS` also names. `CONSTRAINT` says NOT NULL UNIQUE for a plain unique index as well |
| `Index.Size` | NULL, for the same reason as the table size: `M_RS_INDEXES` and `M_CS_INDEXES` |
| `Index.Valid`, `Clustered`, `Predicate`, `ReplicaIdentity`, `Deferrable`, `InitiallyDeferred`, `Options`, `Definition`, `Persistence` | NULL. There is no source |
| `Column.Compression` | `COMPRESSION_TYPE`, lower cased. none is what a row table says |
| `Column.Storage`, `StatsTarget` | NULL |
| `Constraint.Enforced` | `IS_ENFORCED` for a foreign key. NULL for the other kinds, which have no column |
| `Function.Prosrc` | NULL. `Definition` holds the whole statement and there is no body apart from it |
| `Partitions` | new. `SYS.TABLE_PARTITIONS` with the kind of each level from `SYS.PARTITIONED_TABLES`. A partition has a number and no name |
| `NotNulls`, `Policies`, `Inherits` | none. HANA records NOT NULL on the column and not as a named constraint |

## Apache Hive

`models/hive` answers 18 of the 65, against Apache Hive 4.2.1, which is
Tested, and 4.0.1, which runs nightly. It is the
only model here that writes its filter values into the statement, and the
reason is in D78 rather than here.

### It reads sys, and Hive is not Impala

Hive keeps its metadata in a relational metastore that SQL cannot reach.
Hive 3.0 added a `sys` database that exposes that metastore as external
tables over the JDBC storage handler, and those answer ordinary SQL. There
are 57 of them and they are what this model reads.

That is the difference D66 left open. D67 struck Impala because it answers
only through `SHOW` and `DESCRIBE`, which are statements rather than
relations and cannot be filtered, joined or aliased. `sys` is relations, so
Hive passes the test Impala failed.

`sys` is not there when a server starts. The script that creates it ships in
the image and needs a running HiveServer2 to run against, because the tables
are external tables pointed at the metastore. That is neither an image layer
nor a fixture, so `container.Server.Init` was added for it: a command run
once the server answers and before anything reads it. Hive is the only
product that sets it.

### What it answers

Tables, schemas, columns, views, constraints, constraint columns,
partitioned tables, functions, roles, role grants, privileges, comments,
column statistics, access methods, the current schema and the current user.

Three answers are worth naming because Hive arranges the fact differently
from everything else here.

Nullability, a default and a primary key are not properties of a column in
Hive. They are constraints in `KEY_CONSTRAINTS`, so `Columns` reads them
with three outer joins rather than from the column row. They are joins
rather than correlated subqueries because Hive does not take a correlated
scalar subquery in a select list.

A partition key is not a column of the table. It is in `PARTITION_KEYS` and
it does not appear in `COLUMNS_V2` at all, so `Columns` does not report it
and `PartitionedTables` does.
`TestHivePartitionColumnIsNotAColumn` asserts both halves, because a caller
that reads only `Columns` sees a table without its partition column and that
absence otherwise looks like a bug.

A SerDe is how Hive reads and writes a table's rows, which is the question
an access method answers, so `AccessMethods` reports the SerDe classes in
use. Hive keeps no catalog of them, so the query counts the tables that name
each one and a SerDe nothing uses does not appear.

### The trap in KEY_CONSTRAINTS, which cost a round of wrong answers

Hive stores the two sides of a constraint the other way round from how the
names read, and only a foreign key has both. Measured on 4.2.1, with tables
115 and 116:

	kp_pk  type=0  child(tbl=0)    parent(tbl=115)
	kp_fk  type=1  child(tbl=116)  parent(tbl=115)

For a foreign key the child is the table that has the constraint and the
parent is the table it points at. For every other kind the parent is the
table that has it, and the child columns are zero rather than null.

Reading child as "the table this is on" is the obvious mistake and it was
made here first. It is worse than wrong: joining on a zero finds nothing
rather than dropping the row, so every primary key, unique, not null and
default goes silently missing while foreign keys keep working. A schema with
foreign keys in it looks correct. The model normalizes the two sides in a
derived table and the comment on it records the measurement.

### What it cannot answer

40 kinds, and almost all of them because Hive has no such object.

Indexes were removed in Hive 3.0 and there is no statement that makes one,
so `Indexes` and `IndexColumns` have no answer. There are no sequences, no
triggers, no domains, no collations, no user defined types, no operators and
no foreign data of any kind.

`RoutineParameters` has no answer for a reason worth stating: a Hive
function is a Java class registered under a name, and the metastore holds
the name and the class and nothing else. The parameters are in the class.
`Function.Source` carries the class name, which is the nearest thing to a
body Hive has.

`Settings` has no answer because Hive's configuration is read with `SET -v`,
which is a statement rather than a relation, and the `sys` tables do not
carry it.

### What a second opinion found

DeepSeek named three sources and two do not exist:

| Named | What the server says |
| --- | --- |
| `sys.IDXS` for indexes | no such table |
| `sys.TYPES` for a type catalog | no such table |
| `sys.SEQUENCE_TABLE` for sequences | exists, and holds something else entirely |

The third is the most interesting wrong lead in this project so far, because
it is not an invention. `sys.SEQUENCE_TABLE` is real and the name matches
what was asked for. It holds the metastore's internal identifier allocator:

	org.apache.hadoop.hive.metastore.model.MDatabase = 11
	org.apache.hadoop.hive.metastore.model.MRole = 11

A model that reports that as `Sequences` ships a wrong answer that no test
catches, because the query runs and returns rows. A lead that exists is
more dangerous than one that does not, and running it is the only thing that
separates them. See D43.

### What the fixture cannot build

No index, and that is why Hive is not in the cross family comparison in the
root module. D53 asks every fixture for six core objects and one of them is
an index somebody created. Hive builds five.

No function, because `CREATE FUNCTION` registers a Java class by name and a
fixture that makes one depends on a class being on the server's path.
`Functions` is verified to run and returns nothing.

No sequence and no trigger, because Hive has neither.

Every constraint carries `DISABLE NOVALIDATE`, which Hive requires because
it enforces none of them. They are declarations for a planner and the
queries read them as the catalog records them.

### What the conformance test says

Hive's section matches PostgreSQL's on every table, view and column, and
differs in two ways that are both facts.

PostgreSQL reports `has_default=true` on the three `serial` keys, where Hive
agrees with the other nine databases.

Hive reports extra constraint lines. It records NOT NULL and DEFAULT as
constraints where every other database records them on the column, so a
Hive table has `not null` and `default` constraints that the same table
elsewhere does not. Those are additional rows rather than missing ones, and
`Columns` reports the same facts in the same place as everywhere else.

### Which answers depend on who is asking

None of them. The image configures no authorization, so a client states a
principal and HiveServer2 takes it, and the server then allows that
principal everything. That is the measurement rather than a gap in it, and
it is the same shape as Trino and Presto.

A Hive with SQL standard authorization configured answers differently
and nothing here measures that, because the image does not configure it.

### The describe fields (D209)

D198 to D203 gave the root types fields that only PostgreSQL filled. D209 filled
what the metastore has a source for, on 4.2.1 and 4.0.1.

| Field | Apache Hive |
| --- | --- |
| `Table.Owner` | `TBLS.OWNER` |
| `Table.Persistence` | permanent for a table and NULL for a view. A temporary table is not in the metastore |
| `Table.AccessMethod` | the input format class of the storage descriptor. NULL for a view |
| `Table.Size`, `Rows` | the `totalSize` and `numRows` table parameters, which are statistics. NULL for a view, for a table with none, and where Hive writes -1 |
| `Table.Options` | NULL. The parameters mix statistics, times and settings, and no key marks a setting |
| `Constraint.Enforced` | the ENABLE bit of `ENABLE_VALIDATE_RELY`. A NOT NULL, DEFAULT or CHECK can be ENABLE |
| `Partitions` | new. `PARTITIONS`, with `PART_NAME` such as `year=2026` as both the name and the bound |
| `NotNulls` | new. `KEY_CONSTRAINTS` of the type 3, with the column from `COLUMNS_V2`. `Validated` is the VALIDATE bit |
| `Column.Storage`, `Compression`, `StatsTarget` | NULL. The column format is the table's |
| `Function.Prosrc` | NULL. The metastore keeps the Java class, and `Source` holds its name |
| `Policies`, `Inherits`, the index fields | none. Hive 3 removed indexes |

## Presto

Presto is Trino's older self. They forked in 2019 and `models/presto` is a
model of its own rather than a flavor, which D73 decides and measures. Read
the Trino section first: everything there about a catalog being a real level
and about `system.jdbc` spanning catalogs is true here too, and this section
records only where the two differ.

### What it answers

9 of the 65. Catalogs as databases, schemas, tables, columns, views, types,
access methods, privileges and the current user.

Trino answers four more, and each is absent from the product rather than
missing from the model.

Comments has no source. Presto accepts a `COMMENT` clause on
`CREATE TABLE`, keeps nothing readable, and shows nothing in
`SHOW CREATE TABLE`. There is no `system.metadata.table_comments` for the
query to reach, so `Tables.comment` and `Views.comment` are padded absent and
the Comments kind is not registered. `COMMENT ON` is not a statement Presto
has at all: the parser rejects the word, so a column comment cannot be set
either and `system.jdbc.columns.remarks` is always NULL.

CurrentSchema has no expression. Neither `current_catalog` nor
`current_schema` resolves, and nothing in `system.runtime` carries the
session. `current_user` does resolve, so CurrentUser is answered.

Roles and RoleGrants raise rather than answering nothing. This is the
sharpest difference from Trino and the one most likely to surprise. Both
products read the same standard views. Trino's memory connector returns no
rows, which is a supported query with an empty result. Presto's answers:

```
NOT_SUPPORTED: This connector does not support roles
```

D34 says a query dbmeta offers must run, so those two are not registered here.
Privileges reads `information_schema.table_privileges`, which does answer on
the same connector, and returns nothing.

### The version query, and why it is not Trino's

Presto has no `version()` function:

```
Function version not registered
```

So the model reads the coordinator's row from `system.runtime.nodes` instead,
with `WHERE coordinator = true` so that a cluster cannot answer with a
worker's version. `usql` reads the same table for both products and omits that
filter.

The numbering is the other half of D73. Presto is `0.299-7d50721`, which is
release 299 and the build it was cut from. Trino is `483`. There is no version
to compare, only a product to tell apart.

### One driver, and two vendor clients before it

Until 2026-10-07 the test module read the two products through their vendors'
clients, and the two wanted opposite DSNs. `prestodb/presto-go-client/v2`
took `presto://user@host:port/catalog/schema` and refused an `http://` scheme,
and read any unknown query parameter as a session property. `trinodb/
trino-go-client` took `http://` with the catalog and schema as query
parameters. That was a measure of how far the two had drifted.

dburl v0.43.0 names one package for both schemes, `github.com/xo/dbimp/trino`,
and dbimp v0.12.0 holds it (D172). It takes `trino://user@host:port/catalog/
schema` for both products and tells Presto from Trino by `GET /v1/info`. So the
Trino and Presto entries print that form as their DSN and their URL, and the
tests open the driver name `trino` for both. The test image takes no password,
so there is no ordinary user.

`dbmeta` itself is not affected. It depends on nothing and generates its own
DSNs in `container`, which is D19 and hard rule 1.

### What the fixture cannot build

Everything Trino cannot, and two more.

No `NOT NULL`. The memory connector answers "does not support non-null column"
on 0.299, which is the newest release there is, so every column is nullable.
That is the same error that sets Trino's floor at 476, and here there is no
newer release to move to.

No comment of any kind, as above. The view is created without one, because
Presto's `CREATE VIEW` takes `AS` or `SECURITY` after the name and treats
`COMMENT` as a syntax error rather than accepting and dropping it.

### What the conformance test says

Presto builds every core object D53 asks for, including the view, with the
same names and ordinals as every other database. Every column reads
`nullable=true`, which is the fixture difference above, and that is why Presto
has its own section rather than sharing Trino's. It is left out of the
relational agreement count for the same reason Trino is, plus that one.

### Which answers depend on who is asking

None, the same as Trino and for the same reason. The image configures no
authenticator, a client states a principal on every request, and with no
access control plugin the server allows it everything. Only `current_user`
differs for a second principal.

### The describe fields (D210)

None are filled. Presto has no `tables_authorization`, and `SHOW STATS` is one statement for each table.

## Exasol

`models/exasol` answers 26 of the 65. It was run against Exasol 2026.2.0 on
the nano image and 2025.2.1 on the Community Edition machine, and the two
answer identically, so the model has no version fragment. D85 says why there
are two, and D87 records what was decided here. The tests use the driver of dbimp,
`github.com/xo/dbimp/exasol`, and not `exasol-driver-go` (D234). It reads a URL, binds `?` and
returns the types that the model scans, so the answers did not change.

### It reads the EXA_ALL views

Exasol's catalog is a set of system tables in SYS, in three families.
`EXA_USER_` is what the current user owns, `EXA_ALL_` is what it can see, and
`EXA_DBA_` is everything and needs `SELECT ANY DICTIONARY`. The model reads
`EXA_ALL_`, for the reason the Oracle model reads `ALL_`: it answers every
principal with what that principal can see and asks for no special right.

Two queries read `EXA_DBA_`, because nothing else holds the answer.
`RoleGrants` reads `EXA_DBA_ROLE_PRIVS`. The views an ordinary user can read
list only the grants that user holds, which hides every other member's
grants from an administrator too. `UserMappings` reads the connection views,
which carry the remote user and every grant. Both are refused to a lesser
principal, and parity records it.

The system tables are not in the `EXA_ALL_` views, which list what users made.
`with_system` reaches them through `EXA_SYSCAT` and `EXA_SYS_COLUMNS`, for
tables, columns, schemas and the system scripts.

Three things about writing SQL for it, all measured.

An empty string is NULL. Exasol reads `''` as NULL, the way Oracle does, so
`? = ''` is never true and the first version of every filter matched nothing.
A filter tests for NULL instead. A statement that selects `''` for a field
that is always empty gets NULL back, so a plain string field is scanned
through a helper that reads NULL as empty, and three fields where the empty
string means something are restored from the row: identity and generated,
which are empty for an ordinary column, and a foreign key's catalog.

A correlated `EXISTS` in a select list is refused as not supported, so the
primary key flag on a column is a join. A correlated scalar subquery is
accepted, which is how the partition key and a virtual schema's properties are
folded into one row.

`CONNECT BY` runs before `WHERE`. Splitting the `SCRIPT_LANGUAGES` parameter
over `EXA_PARAMETERS` itself multiplied every level by every parameter and
returned 182 rows for four languages, so the parameter is selected first and
the generator runs over that one row.

### What it answers

Tables, schemas, columns, views, indexes, index columns, constraints,
constraint columns, partitioned tables, comments, functions, aggregates,
types, languages, roles, role grants, privileges, settings, the database,
foreign data wrappers, foreign servers, user mappings, foreign tables, the
current schema and the current user.

Five need a sentence.

A virtual schema is how Exasol reaches data it does not hold, and it lines up
with PostgreSQL's foreign data in three places:

| Kind | Exasol |
| --- | --- |
| `ForeignDataWrappers` | an adapter script, `EXA_ALL_SCRIPTS` where `SCRIPT_TYPE` is ADAPTER |
| `ForeignServers` | a virtual schema, with its properties as the options |
| `ForeignTables` | a virtual table, which belongs to its virtual schema |

The fixture builds all three with a Lua adapter that answers with one fixed
table, because Lua runs inside the engine and needs no language container and
no second database.

`UserMappings` is a connection granted to a principal. A connection holds an
address and the credentials to use it, which is what a mapping records: who
can reach a source, and as whom. Its server is the connection, which is not
the virtual schema `ForeignServers` reports, and a virtual schema names the
connection it uses in its `CONNECTION_NAME` property.

`Aggregates` is a set UDF that returns one value. Exasol's own documentation
calls that an aggregate. A set UDF that emits rows is a function returning a
set.

`Indexes` reports the indices the engine made. Exasol has no `CREATE INDEX`:
it builds an index for a key and others as joins need them, and drops the ones
that go unused. Nothing names one, so the object id is the name and `REMARKS`,
the engine's own description such as `GLOBAL INDEX (COUNTRY,AREA)`, is the
comment.

`IndexColumns` reads the column list out of `REMARKS`, which is the only place
it is kept. The names in it are not quoted, and a column can be called `a,b`,
so the list is matched against the table's real columns rather than split on
its commas. A name that is itself two other column names joined by a comma
matches twice. Nothing short of that can be misread.

### What it cannot answer

31 kinds. Most are absent from the product.

There is no sequence, trigger, domain, enumerated type, collation, tablespace,
access method, conversion, cast, large object, event trigger, operator,
operator class or family, extension, extended statistic, publication,
subscription, text search object or default privilege. There is no unique or
check constraint: both are refused as not supported, on both releases. An
identity column is the only generator.

Three are present and unreachable, which is a different thing.

`RoutineParameters` has no answer because Exasol keeps the parameters of a
function or a script only inside its text. Parsing them out was considered and
left alone: a SQL function's text is stored as written, and a type such as
`DECIMAL(18, 0)` puts a comma inside a parameter, so a split is wrong exactly
where it matters. `Function.ArgTypes` and `Function.ResultType` are empty for
the same reason, and the text is in `Function.Definition`.

`ColumnStats` has no answer because Exasol exposes no planner statistic.
`EXA_ALL_COLUMN_SIZES` holds the memory and raw size of each column, which is
storage rather than a statistic about the values.

`RoleSettings` has no answer. A user or a role carries a consumer group, which
limits resources, and that is not a configuration setting a role applies.

### What a second opinion found

Gemini and DeepSeek were both asked about the thirty kinds, given the whole
list of system tables. They agreed on three leads.

| Lead | What the server says |
| --- | --- |
| `UserMappings` from `EXA_DBA_CONNECTIONS` and `EXA_DBA_CONNECTION_PRIVS` | both exist with the columns named. It is the source `UserMappings` reads |
| `IndexColumns` by parsing `REMARKS` | the list is there and unquoted. Splitting it is wrong for a column named `a,b`, so the model matches it against the real columns |
| `RoutineParameters` by parsing the text | the text is there. Rejected, above |

DeepSeek also offered `ColumnStats` computed from the data, with `COUNT(*)`
and `COUNT(DISTINCT)` per column. That is a scan that grows with every row in
the database, which D47 forbids.

Both invented a column. Gemini said `EXA_ALL_COLUMN_SIZES` has
`AVERAGE_COLUMN_SIZE` and `COMPRESSED_SIZE`. It has `RAW_OBJECT_SIZE` and
`MEM_OBJECT_SIZE`. DeepSeek said `EXA_ALL_INDICES` has `ROOT_SCHEMA` and
`ROOT_NAME`. It has `INDEX_SCHEMA` and `INDEX_TABLE`. A query built on either answer does not run.
See D43.

### What the fixture cannot build

No index, because there is no `CREATE INDEX`, so the core object
`book_published` does not exist here and the fixture is not in the root
module's fixture test. No unique or check constraint on `book`. No user,
because a user outlives the schema, so parity makes its own.

A view comment is written in the `COMMENT IS` clause of `CREATE VIEW`, because
`COMMENT ON VIEW` is refused.

### What conformance says

Exasol agrees with the relational databases on 22 of their 23 lines. The one
it lacks is the unique constraint on `book.title`, which cannot be built, so
it is in `agreementExcluded` with that reason. It also reports each NOT NULL
as a named constraint of its own, which is how Exasol keeps them, and a view
column as nullable.

### Which answers depend on who is asking

Measured with SYS, the owner of the fixture schema and a grantee on it.

`RoleGrants` and `UserMappings` are refused to both, as above.

`Privileges` returns fewer rows to the grantee, because `EXA_ALL_OBJ_PRIVS`
lists the grants a user made, received, or owns the object of.

`ForeignServers` returns the same row with its options absent, because a
virtual schema's properties are shown to its owner and not to a user who can
only see the schema.

`CurrentSchema` returns no row for a new session, which has opened no schema.
That is the session rather than the principal.

On 2025.2.1 `ForeignDataWrappers` returns fewer rows as well. The Community
Edition ships eleven adapter scripts in `VS_ADAPTERS`, which SYS owns and a
principal that owns another schema cannot see. The nano image ships none, so
the difference is in what the machine holds and not in the query.
`test/testdata/parity.txt` has an `exasol@2025` section for it.

### The describe fields (D209)

D198 to D203 gave the root types fields that only PostgreSQL filled. D209 filled
what Exasol has a source for, on 2026.2.0. The 2025.2.1 Community Edition
machine is Verified and was not run.

| Field | Exasol |
| --- | --- |
| `Table.Owner` | `TABLE_OWNER` and `VIEW_OWNER` |
| `Table.Persistence` | permanent for a table. NULL for a view, a virtual table and a system table |
| `Table.Rows` | `TABLE_ROW_COUNT`, which is exact |
| `Table.Size` | `MEM_OBJECT_SIZE` of `EXA_ALL_OBJECT_SIZES`. `RAW_OBJECT_SIZE`, the size before compression, is not returned |
| `Table.Options` | `distribute_by` and `partition_by` with the key columns |
| `Table.AccessMethod` | NULL. Exasol has one engine |
| `Index.Owner`, `Size` | `INDEX_OWNER` and `MEM_OBJECT_SIZE` of `EXA_ALL_INDICES` |
| `Index.Valid`, `Clustered`, `Predicate`, `Using`, `Options`, `Definition`, `Persistence` | NULL. There is no source |
| `Constraint.Enforced` | `CONSTRAINT_ENABLED`, which is false for a constraint declared DISABLE |
| `NotNulls` | new. `EXA_ALL_CONSTRAINTS` of the type NOT NULL, with the column from `EXA_ALL_CONSTRAINT_COLUMNS` |
| `Column.Storage`, `Compression`, `StatsTarget` | NULL |
| `Function.Prosrc` | NULL. Exasol keeps the whole statement, which is `Definition` |
| `Partitions` | not answered. An Exasol partition is a key and not an object, and `PartitionedTables` has the key columns |
| `Policies`, `Inherits` | none |

## Vertica

`models/vertica` answers 28 of the 65 on 25.1, 26 on 9.1.0 and 10.1.1, and 25
on 7.2.1. The older releases have no triggers and no per user settings to read,
and 7.2.1 has no row access policy. It was run
against 7.2.1, 9.1.0, 10.1.1 and 25.1.0, all four community images, and D88
records why those. The images are copies in `docker.io/usql/vertica`, which
D100 records.

### It reads v_catalog

Vertica began as a fork of PostgreSQL and its catalog is the `v_catalog`
schema, with the engine's running state in `v_monitor`. Every query here reads
those two. Much of it will look familiar to anybody who knows PostgreSQL:
`vsql` is `psql`, names fold to lower case, the empty string and NULL are
different values, and `standard_conforming_strings` decides how a literal is
escaped.

The catalog grows with the product: `v_catalog` has 56 tables on 7.2, 73 on
9.1, 89 on 10.1 and more on 25.1, where namespaces arrived. The base names,
a table's schema and name, are the same on every release, so most queries
need no fragment, and the gates are few:

| From | What |
| --- | --- |
| 9.1 | CHECK constraints, SET USING columns, a function's owner, a user's connection limit |
| 10.1 | `LISTAGG`, and a comment on a table column |
| 25.1 | PL/vSQL, a procedure's language, owner and security, triggers, per user settings |

The 25.1 row is measured present there and absent on 10.1, and nothing
between is measured.

Four things about writing SQL for it, all measured.

A CASE whose branches are literals of different lengths is typed CHAR of the
longest, so `unique` arrived padded to the width of `primary key`. Every CASE
that produces text is cast to VARCHAR.

`CURRENT_SCHEMA()` is allowed only in the outermost select list, not in a
WHERE, a join or a derived table, so CurrentSchema cannot join the schema's
row and leaves the owner and comment to Schemas.

A subquery beside GROUP BY is refused, so the access policies Privileges folds
in are aggregated first and joined.

`vertica-sql-go`, the driver `usql` uses, splits a statement at every
semicolon before it sends it, so a SQL function, whose body needs one, cannot
be created through it at all. `vsql` creates it without complaint.

### What it answers

Tables, schemas, columns, views, indexes, index columns, constraints,
constraint columns, sequences, partitioned tables, comments, functions,
aggregates, types, triggers, roles, role settings, role grants, privileges,
databases, tablespaces, settings, foreign servers, foreign tables, the
current schema and the current user.

Six answer with an analogue:

| Kind | Vertica | Why it is a fair answer |
| --- | --- | --- |
| `Indexes` | projections | Vertica has no index. A projection is a stored, sorted and segmented copy of some or all of a table's columns, and the optimizer chooses between projections the way another database chooses between indexes. `IndexColumns` is the projection's columns, in the order it stores them |
| `Tablespaces` | `storage_locations` | a storage location is a directory on a node that holds data or temporary files, and a label lets a storage policy send a table's data to it |
| `ForeignServers` | `hcatalog_schemata` | an HCatalog schema reaches the tables of a Hive metastore through the HCatalog connector: one source and the settings to reach it |
| `ForeignTables` | external tables | an external table reads its rows from files through the COPY statement it was created with, and names them directly, so there is no server object |
| `RoleSettings` | `user_configuration_parameters` | a user's own value for a parameter, set with ALTER USER ... SET. A role carries none. 25.1 alone |
| `Triggers` | `stored_proc_triggers` | Vertica's own word, and it runs a stored procedure on a schedule rather than on a change to a table, so it names no table. 25.1 alone |

A NOT NULL is not reported as a constraint. Vertica records one per column in
`constraint_columns`, all under the name `C_NOTNULL`, and not in
`table_constraints`, which is the way PostgreSQL treats one too. A key is
recorded whether or not it is enabled, and Vertica checks one only when it
is. An identity column refuses an explicit value, so its identity kind is
always.

Before 10.1 there is no string aggregate, so Privileges returns a row per
object and grantee there, and a row per object from 10.1.

### What it cannot answer

30 kinds. Most are absent from the product: domains, enumerated types, casts,
operators, operator classes and families, collation objects, conversions,
large objects, event triggers, extensions, extended statistics, publications,
subscriptions, text search objects, access methods, user mappings and foreign
data wrappers. HCatalog is one connector rather than a catalog of them.

Four are present in some form and rejected, which is a different thing.

`RoutineParameters` has no answer. A routine's arguments are one comma
separated list of types on its own row, and the named parameters a library
function declares in `user_function_parameters` are `USING PARAMETERS`
options rather than arguments. A report of those as parameters answers a
different question.

`DefaultACLs` has no answer. A schema can make new objects inherit its grants,
and `inherited_privileges` and `inheriting_objects` record that from 10.1, but
that is a flag on the schema rather than a default privilege a principal
sets.

`Languages` has no answer. Nothing lists them. A library records its SDK
version and not the language it was written in.

`ColumnStats` has no answer. `table_statistics` holds row counts, and nothing
exposes a column's null fraction, distinct count or most common values.

### What a second opinion found

Gemini and DeepSeek were both asked about the thirty one kinds, given the
table lists. Gemini's answer was cut off after the first rows, which agreed
with the table above. DeepSeek named three leads.

| Lead | What the server says |
| --- | --- |
| `RoleSettings` from `user_configuration_parameters` | it exists on 25.1 with user_name, parameter_name and current_value. It is the source `RoleSettings` reads |
| `Languages` from a `language` column of `user_libraries` | there is no such column |
| `ColumnStats` from `table_statistics` | it holds row counts, not column statistics |

Running the second one was the only way to tell it was invented. `ForeignServers`
from `hcatalog_schemata` was found here rather than offered, while checking
DeepSeek's answer that foreign servers are absent.

### What the fixture cannot build

No SQL function, because of the driver's semicolon split above. Functions
reads the functions the packages Vertica installs at startup provide, and a
PL/vSQL procedure on 25.1.

No HCatalog schema, because the images carry no HCatalog connector and CREATE
HCATALOG SCHEMA is refused, so `ForeignServers` is verified to run and return
nothing.

On 7.2 and 9.1 the view is created before `book_published`, because a table
with a projection somebody made gets no superprojection until it holds rows,
and a view over a column no projection carries is refused. On 7.2 there is no
CHECK constraint.

### What conformance says

Vertica agrees with the relational databases on all their lines, including
the unique and check constraints on `book`. 7.2 has no CHECK constraint, so
it has a section of its own, `vertica@7`, one line shorter. It is the first
product to need one in `conformance.txt`, and D88 records the change.

### Which answers depend on who is asking

Measured with `dbmeta`, which holds PSEUDOSUPERUSER because `dbadmin` has no
password, the owner of the fixture schema and a grantee on it.

`RoleSettings` is refused to both on 25.1, because a lesser principal cannot
read `user_configuration_parameters`.

`Roles`, `RoleGrants` and `Schemas` return fewer rows to both, because each
view shows a principal what it can reach. The grantee also sees fewer
comments, functions and sequences.

`Settings` returns the same rows with different values, because
`configuration_parameters` hides the values a superuser alone can see.

On 10.1 `Privileges` is refused to both, because a lesser principal cannot read
`access_policy`, which the query joins for its policies. 25.1 serves it. Before
10.1 the query does not read that view.

### The describe fields (D208)

| Field or kind | What Vertica fills |
| --- | --- |
| `Table.Owner` | `owner_name` of a table and of a view. NULL for a system table |
| `Table.Persistence` | `temporary` for a global or local temporary table, and `permanent`. NULL for a view and a system table |
| `Table.Options` | `partition_by=expression` for a partitioned table |
| `Table.Size`, `Table.Rows` | NULL. A table is stored only as its projections, and the numbers are in `v_monitor.projection_storage`. A subquery on it for every table made a read of one table 3 times slower at 300 tables and 10 times slower at 3000 tables. A projection of a replicated table is also repeated on every node, so a sum of rows is not the rows |
| `Table.RowSecurity` | NULL. The row policies are in `v_catalog.access_policy`, which shows a user only what the user can see. The grantee read `false` where the administrator read `true`, which is a wrong answer |
| `Column.Compression` | NULL. `projection_columns.encoding_type` is read through a subquery that made a read of one table 8 times slower at 300 tables and 22 times slower at 3000 tables |
| `Index.Size` | the sum of `used_bytes` of the projection on every node. The rows of a projection are in the same view and `Index` has no field for them. This doubles the cost of a read that already scans `projections` |
| `Index.Valid`, `Options` | `is_up_to_date`, and `segmented_by=expression` or `unsegmented` |
| `Constraint.Enforced` | `is_enabled`. A key is enabled only when the statement said ENABLED. A check constraint is always enabled. A foreign key has no value, so it is NULL |
| `Function.Prosrc` | `function_definition`, the same text as `source` |
| `Partitions` | new. One row for each partition key of each table, from `v_monitor.partitions` joined to `projections`. The fixture inserts with a DIRECT hint, because the older releases keep a small insert in memory and list no partition until a moveout |
| `Policies` | new, from 9.1. One row for each row access policy of `v_catalog.access_policy`. A policy has no name, so the name is its object id. A column policy is not listed. `Enabled` is `is_policy_enabled`, which holds the words Enabled and Disabled, and a policy that is disabled is a row with `Enabled` false (D211). On 9.1 and 10.1 an ordinary user is refused `access_policy`, and on 25.1 the user reads only the policies it can see |
| `Column.Storage`, `StatsTarget`, `Index.Owner`, `Predicate`, `Clustered` | NULL. There is no source |
| `NotNulls`, `Inherits` | none. A NOT NULL has the one name C_NOTNULL for every column |

## Couchbase

`models/couchbase` answers 12 of the 65 on 7.6 and 8.0. It was run against
7.6.12 and 8.0.3 through `github.com/xo/dbimp/couchbase`, the driver `usql`
uses. 7.2.9 reports that it is too old, and D104 says why.

### It reads the system keyspaces

Couchbase has no `information_schema`. Its catalog is the `system` namespace,
which SQL++ reads like any other keyspace. Every query is one statement over
`system:buckets`, `system:all_scopes`, `system:all_keyspaces`,
`system:indexes`, `system:functions`, `system:sequences`, `system:user_info`
or `system:applicable_roles`.

A bucket is the database, a scope is the schema and a collection is the
table. So a table's catalog is its bucket and its schema is its scope. The
default collection of a bucket is reported as the scope `_default` and the
collection `_default`, which is how Couchbase addresses it.

Four things about writing SQL++ for it, all measured:

1. A field that is absent from a document is MISSING, and SQL++ leaves it out
   of the row. It does not write it as null. A driver takes its columns from
   the row, so a row that drops a key drops a column. Every expression that can
   be MISSING is written `IFMISSING(x, NULL)`. `docs/NULLS.md` has the rule.
2. `REGEXP_LIKE` matches the whole string. `REGEXP_CONTAINS` finds a
   substring. The descending flag of an index key uses the second.
3. `REGEXP_REPLACE` has no backreference such as `$1`. An index key loses its
   backticks through `TRIM(x, '`')`.
4. The reserved words differ between releases. `role` is reserved on 7.6 and
   8.0, and 8.0 also reserves `roles`. Both are quoted with backticks.

### What it answers

Databases, schemas, tables, indexes, index columns, functions, routine
parameters, sequences, roles, role grants, privileges and the current user.

Five answer with an analogue:

| Kind | Couchbase | Why it is a fair answer |
| --- | --- | --- |
| `Roles` | users | a Couchbase user is the principal that logs in and holds grants. A Couchbase role, such as `query_select` on a bucket, is a privilege, and `Privileges` returns those |
| `RoleGrants` | groups | a user is a member of groups and has their roles. SQL++ has no catalog of groups, so a group appears only as what a user is a member of |
| `Privileges` | `system:applicable_roles` | one row for each object, with each role held on it. The object is a bucket, a scope or a collection, and nothing for a role on the whole cluster |
| `Functions` | SQL++ user defined functions | a function is inline, with one expression as its body, or JavaScript, whose body is in a library. Its parameters have names and no types |
| `Sequences` | `system:sequences` | 7.6 added sequences. The catalog names a sequence by its scope and loses its bucket, so the bucket is not reported |

A variadic function has no `parameters` field at all, and a function with no
parameters has the empty list. So a missing list is reported as the one
parameter `...`, which is how CREATE FUNCTION writes it.

### What it cannot answer

44 kinds. Most are absent from the product: views, constraints of any kind,
domains, enumerated types, casts, operators, operator classes and families,
collations, conversions, large objects, triggers, event triggers, extensions,
extended statistics, publications, subscriptions, text search objects, access
methods, tablespaces, partitioned tables, foreign data wrappers, foreign
servers, foreign tables, user mappings, default privileges, languages,
aggregates, comments and the current schema.

Five are present in some form and rejected, which is a different thing.

`Columns` has no answer. A document has no fixed shape, so a collection has no
columns. INFER samples documents and guesses a schema from them. That is a
guess about the data and not a fact of the catalog. It is also one statement
per collection, and on 7.6 it cannot be a subquery at all, so it fails the
cost test of D47.

`ColumnStats` has no answer. UPDATE STATISTICS keeps its results in the
internal collection `_system._query` of each bucket, with each histogram as
base64 JSON. A statement names the bucket it reads in FROM, so no one
statement covers every bucket. The records of a dropped collection also stay
behind. `system:dictionary`, which is the view of that data, fails with a
server panic on 7.6.12 and 8.0.3, before and after UPDATE STATISTICS.

`Settings` has no answer. 8.0 adds `system:bucket_info`, which holds the
configuration of each bucket, and `system:aus`, which holds the configuration
of automatic statistics. Neither is the configuration of the server. The
query service keeps its own on a REST endpoint that SQL++ cannot read.

`RoleSettings` has no answer. A user carries its roles and no setting.

`Triggers` has no answer. The Eventing service runs a function when a document
changes, and SQL++ has no catalog of Eventing functions.

### What a second opinion found

Gemini and DeepSeek were both asked about the 43 kinds, with the system
keyspaces named. Both answers were short, and neither gave a lead that the
table above did not already reject. The leads they named:

| Lead | What the server says |
| --- | --- |
| `Columns` from INFER | it works, and it is a sample. On 7.6 it cannot be a subquery, and on 8.0 it takes only a fixed keyspace, so one statement cannot read every collection |
| `ColumnStats` from `system:dictionary` | the keyspace exists and every read of it fails with a server panic |
| `CurrentSchema` from `CURRENT_SCOPE()` | there is no such function on either release |
| `Settings` from `system:settings` | there is no such keyspace |
| `Settings` from `system:bucket_info` | it exists on 8.0 only, and it holds bucket configuration |
| `RoleSettings` from `system:user_info` | it holds the user's roles, which `Privileges` already reads |
| `Views` from `system:views` | there is no such keyspace |

The statistics in `_system._query` were found here, while checking the
`system:dictionary` lead.

### What the fixture builds, and what it cannot build

The fixture builds a scope, `dbmeta_fixture`, in the bucket `dbmeta` that the
`dbrun` setup makes. SQL++ can create a scope and cannot create a bucket. The
scope holds the four core collections, a primary index, `book_published`, an
index with a descending key and an index over an array, an inline function
with two parameters, a variadic function, a sequence, and a grant to the
ordinary user on one collection.

A collection that CREATE COLLECTION made is not visible to the next statement
for a moment, so the test tries such a step again until it succeeds.

It cannot build a group or a user, because SQL++ has no statement for either,
so `RoleGrants` is verified to run and returns no fixture row. It cannot
build a JavaScript function, because the library that holds its body is
managed through REST.

### What the conformance test says

The `couchbase` section holds the four core collections and nothing else.
There is no column catalog and no constraint, so the relational lines have no
Couchbase answer, and `agreementExcluded` says so.

### Which answers depend on who is asking

Couchbase has no containment and no object owner. A user belongs to the
cluster and holds roles, so there is one lesser kind of principal. It is
`dbmeta_user`, which the `dbrun` setup makes with select, insert, update and
delete on the bucket, and `query_system_catalog`.

`Roles`, `RoleGrants` and `Privileges` are refused to the user, because each
reads `system:user_info` or `system:applicable_roles`. The error names a
different role on each release, so 8.0 has a section of its own,
`couchbase@8`.

`Functions` and `RoutineParameters` return no rows to the user.
`system:functions` shows only the functions a user can run or manage, and the
user holds no function role. Measured with a scope function and a global one,
both of which the administrator sees and the user does not.

### The describe fields (D210)

Indexes reads `condition`, `state`, `metadata.definition` and `with` as `Predicate`, `Valid`, `Definition` and `Options`. 7.6 has neither the statement nor the settings, so those two are NULL there. Functions reads `Prosrc`. The size and the rows are in `system:keyspaces_info`, which asks the data service once for each collection, so they are not read.

## Which answers depend on who is asking

Every query has been asked as the administrator and as each lesser kind of
principal the product has. D61 requires it of every dialect,
`test/parity_test.go` does it, and `test/testdata/parity.txt` is the record.

The principals get the same rights over the fixture schema, so the only thing
that varies is what kind of principal they are.

| Database | Principal | Queries that answer differently |
| --- | --- | --- |
| Oracle 26ai | local user | none |
| SQL Server 2022 | contained user | none |
| SQL Server 2022 | server login | `roles` |
| SQL Server 2012, 2014, 2016 | both | the same as 2022 |
| SQL Server 2008 R2 | server login | the same as 2022. It has no contained user |
| PostgreSQL 18 | schema owner | `settings`, `tablespaces` |
| PostgreSQL 18 | grantee | `settings`, `tablespaces` |
| Cassandra 5.0 | granted role | `privileges`, `role_grants`, `roles`, `settings` |
| ScyllaDB 2025.1, 2026.1, 2026.2, 2026.3 | granted role | the same as Cassandra, plus `role_settings` |
| ClickHouse 26.9 | granted user | `constraints`, `databases`, `foreign_servers`, `index_columns`, `indexes`, `privileges`, `role_grants`, `roles`, `tablespaces` |
| MySQL 8.4 | grantee | `foreign_servers`, `functions`, `role_grants`, `roles`, `user_mappings` |
| MariaDB 13.0 | grantee | `aggregates`, `column_stats`, `foreign_servers`, `role_grants`, `roles`, `user_mappings` |
| MariaDB 10.6, 10.11 | grantee | the same, plus `functions` |
| PostgreSQL 10, 11, 12, 13 | both | each has a section of its own, and `subscriptions` is refused on some. One release answers differently, below, says why |
| Firebird 3.0, 4.0, 5.0 | grantee | `roles`, `settings` |
| Apache Hive 4.2 | other principal | none. The image configures no authorization, so every principal is allowed everything |
| SAP HANA 2.0 SPS 08 | grantee | `collations`, `databases`, `foreign_data_wrappers`, `functions`, `privileges`, `role_grants`, `roles`, `settings`, `triggers`, `views` |
| Exasol 2026.2.0 | schema owner | `current_schema`, `foreign_servers`, `role_grants`, `user_mappings` |
| Exasol 2026.2.0 | grantee | `current_schema`, `foreign_servers`, `privileges`, `role_grants`, `user_mappings` |
| Exasol 2025.2.1 | both | the same, plus `foreign_data_wrappers`, because the Community Edition ships adapter scripts that SYS owns |
| Vertica 25.1 | schema owner | `role_grants`, `role_settings`, `roles`, `schemas`, `settings` |
| Vertica 25.1 | grantee | the same, plus `comments`, `functions`, `privileges` and `sequences` |
| Vertica 10.1 | both | the same as 25.1 without `role_settings` and `schemas`, a grantee sees every function, and `privileges` is refused, because a lesser principal cannot read `access_policy` |
| Vertica 7.2, 9.1 | both | the same as 25.1 without `role_settings`, a grantee sees every function, and a grantee sees fewer rows in `privileges` rather than different values |
| Trino 476, 483 | other principal | none. The image configures no authenticator |
| Presto 0.299 | other principal | none, for the same reason |
| Couchbase 7.6, 8.0 | ordinary user | `functions`, `privileges`, `role_grants`, `roles`, `routine_parameters` |
| Neo4j 5.26, 2026.09 | ordinary user | `privileges`, `role_grants`, `role_settings`, `roles`, `settings` |

`current_user` and `current_schema` are left out of the table and are in the
file. They answer a question about the connection, so a run where they agree
is the fault.

The releases on Windows machines are measured too. They are Verified rather
than Tested, so `dbrun test sqlserver-2008R2` and its siblings run it rather
than CI. A contained database arrived in SQL Server 2012, so 2008 R2 skips that
scene and says so instead of failing.

Every dialect has a target or a recorded reason for having none, and
`TestEveryDialectIsMeasuredForParity` fails when one has neither. SQLite and
DuckDB have no user. Snowflake has a grantee, a visitor and a stranger, which
are roles of users that the test makes (D193). Impala and
Vitess run with no authentication, so every user is the same principal.
InfluxDB 3 Core has one kind of token, the administrator's (D152).
`parityExempt` in `test/parity_test.go` holds each reason.

### One release answers differently

`test/testdata/parity.txt` has a section per product, and fifteen releases
have one of their own: `postgres@10`, `postgres@11`, `postgres@12`,
`postgres@13`, `mariadb@10`, `exasol@2025`, `vertica@7`, `vertica@9`,
`vertica@10`, `couchbase@8`, `cockroachdb@24.3`, `cockroachdb@26.2`,
`questdb@9.4`, `tidb@8.5` and `influxql@1`. A section named for a release wins over the shared one, and
a name carrying a minor wins over one carrying only the major.

PostgreSQL restricts a column of `pg_subscription` from an ordinary role. The
`Subscriptions` query reads `subsynccommit` as `synchronous`, so the whole
query is refused where that column is restricted. Measured directly, with a
role granted nothing but LOGIN:

	13.23   SELECT count(*) FROM pg_subscription    ok
	13.23   SELECT count(subsynccommit) ...         permission denied
	14.24   SELECT count(subsynccommit) ...         ok

So an ordinary role is refused on 10, 11, 12 and 13, and served from 14. A
superuser reads it on every release, and `pg_subscription` does not exist
before 10, where `Subscriptions` reports `TooOld` instead.

An earlier version of this section said the grant widened in 13 and that the
same role is served from 13 on. That was wrong by one release and it went
unnoticed because only 12 had a section: 10, 11 and 13 were never measured
until D69 made the nightly matrix read the release list, and all three failed
the first night it did. The four sections now carry the measurement.

PostgreSQL 10 words the refusal differently from the rest, "permission denied
for relation" where 11 and later say "for table", which is why its section
cannot be shared with theirs.

The query is not gated for this. A superuser on 12 can read the column, and
padding it withholds a fact from the caller who is allowed it, which rule
13 forbids. What was wrong was the file claiming one answer covers every
release of a product.

It was found by CI rather than by the cross-release check that was supposed to
catch it. That check compared 9.6 against 18, and 9.6 has no `pg_subscription`
at all, so the query was never asked and the difference never showed.

ClickHouse had a sixth section, `clickhouse@25.3`, and it is gone because it
was never a difference in the product. It recorded that 25.3 did not refuse
`foreign_servers` when every later release did, and the truth was that the
query did not run on 25.3 at all: it read `n.create_query`, which arrived in
25.6, so the server was not able to resolve the identifier and there was no answer
to compare. A query that fails for everybody looks like a query that treats
everybody alike.

With the column gated the query runs on 25.3, a granted user is refused there
exactly as on every later release, and the shared section covers it. D83 has
the measurement.

A section can name a minor, and four do: the CockroachDB, QuestDB and TiDB sections above. The mechanism
was added for this section and it was right for a reason that outlives it:
the first attempt called it `clickhouse@25`, which is what every other
product's section does, and that broke 25.8, because ClickHouse versions by
calendar and 25.3 and 25.8 are both major 25. Any product that versions that
way will need it again. SAP HANA is the nearest, since every one of its
releases is major 2. Nothing moves for PostgreSQL, Oracle or SQL Server,
where the major does identify a release line.

MariaDB 10.6 is the second, and it is `Functions`.
`information_schema.ROUTINES` reports `routine_definition` as NULL to a user
that cannot read the routine's source, and the query selects that column as
`source`, so a grantee gets the same rows with the definition absent. MariaDB
11.3 made `SHOW CREATE ROUTINE` a grantable privilege, and
`GRANT ALL PRIVILEGES` on a database carries it, so 13.0 serves the definition
to the same principal. 10.6 has no such privilege to grant, and only the
definer or a user that can read `mysql.proc` sees it there.

Nothing is gated for this either, and for the same reason: an administrator on
10.6 reads the column, so padding it withholds a fact from a caller who is
allowed it.

This one was hidden rather than missed. The workflow ran MariaDB with
`-run 'MySQL|InformationSchema|Conformance'`, which no parity test matches, so
parity had never been asked of MariaDB at all. D69 replaced those twelve
hand written jobs with one that runs the whole suite per release, and this was
the first thing it found.

### What each one means for a consumer

The MySQL dialect is the one to plan for. Queries are refused outright, not
narrowed, for a user holding ALL PRIVILEGES on its own database: six on
MariaDB and four on MySQL. They read `mysql.proc`, `mysql.column_stats`,
`mysql.servers`, `mysql.roles_mapping`, `mysql.role_edges` and `mysql.user`,
which are tables in the `mysql` database rather than views that filter
themselves, so the server answers with error 1142 and the query fails. A
consumer that offers these to an ordinary user has to be ready for the error.

The two products do not agree with each other, which is why the record names
the product rather than the dialect. `mysql.proc` was removed in MySQL 8.0 and
MariaDB still has it, so `aggregates` is refused on one and answered on the
other.

PostgreSQL refuses `tablespaces`, because reading `pg_global` needs a
privilege an ordinary role does not have, and narrows `settings` from 404
parameters to 375 because `pg_settings` hides the rest. Everything else
answers identically, because the model reads `pg_catalog`, which shows every
row to everybody. That is why `information_schema` is not used for the
PostgreSQL model: it filters by privilege and `pg_catalog` does not.

SQL Server narrows `roles` for a server login, from 8 to 6, because a login
cannot see every server role. A contained user answers identically to the
sysadmin on every query, which is the cleanest result of the four.

Oracle answers identically on every query for a local user that owns the
objects. That is worth saying plainly, because D60 began from the worry that
`ALL_` views under-report. They do, but only for a caller asking about
a schema it has no privilege on, which is a different question, and that
measurement is what closed D60 with no change to the model.

## What every database agrees on

`TestConformance` builds the same core schema everywhere and compares the
canonical answer against `test/testdata/conformance.txt`, which is checked in.
23 of the canonical lines are identical across every section that
`agreementExcluded` in `test/conform_test.go` does not name. The table below
holds the differences between the first five, PostgreSQL, MariaDB, MySQL,
SQLite and DuckDB. Every difference is real and none is a fault.

| Difference | Why |
| --- | --- |
| SQLite reports a primary key column as nullable | An `INTEGER PRIMARY KEY` in SQLite genuinely accepts NULL unless the column is declared NOT NULL. It is the oldest surprise in SQLite and it is not a fault. |
| PostgreSQL reports a default on a primary key | `serial` is implemented as a `nextval` default. `AUTO_INCREMENT` and DuckDB's plain key are not defaults. |
| MariaDB reports the literal `NULL` as the default of a nullable column with no default | MariaDB is saying the column defaults to NULL, which is true. MySQL reports no default, as PostgreSQL, SQLite and DuckDB do. `TestMySQLNullDefault` pins both. |
| MariaDB reports a view's columns as not nullable | The others say nullable. Each is inferring from the view body differently. |
| Of those five, only PostgreSQL and DuckDB report the columns of a check constraint | `KEY_COLUMN_USAGE` does not cover a check, and SQLite publishes nothing about one. |

The comparison is over the portable facts only: whether a column accepts NULL,
where it sits, whether it is in the primary key, whether it has an explicit
default, and which columns a constraint covers in what order with what it
references. A type spelling, a rendered default and a rendered definition are
per product and are dropped, and `test/canonical.go` records every one with the
reason.

## The seven kinds psql has no command for

These exist because a consumer measured in D46 needs them. D47 allows them:
`psql` sets the object model and does not set the column set, and `psql`
renders a constraint and a routine signature as text because a person is
reading them.

| Question | PostgreSQL | MariaDB | MySQL | SQLite | DuckDB | SQL Server | information_schema |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `ConstraintColumns` | yes | yes | yes | yes | yes | yes | yes |
| `RoutineParameters` | yes | yes | yes | no | yes | yes | yes |
| `Views` | yes | yes | yes | yes | yes | yes | yes |
| `CurrentSchema` | yes | yes | yes | yes | yes | yes | yes |
| `CurrentUser` | yes | yes | yes | no | yes | yes | yes |
| `EnumValues` | yes | no | no | no | yes | no | no |
| `ColumnStats` | yes | yes | no | no | no | yes | no |

The table has a column for each of the first seven products, and the section of each later model
says which of the seven it answers. The SQL standard defines
`key_column_usage`, `parameters`, `views` and `schemata`, so a database close
to the standard answers those four. That was not the expectation: the two the
consumers wanted most turned out to be the two the standard already had.

`CurrentUser` is answered by every model but seven: SQLite, rqlite and
libSQL, which have no users in SQL, Cassandra, which has no CQL expression for
it, InfluxDB 3 and InfluxQL, where no statement names a user, and YDB, where
the function for it returns the empty string. It is
the one question here that reads no catalog at all on the shared model, because
`CURRENT_USER` is a standard SQL expression rather than a view. It came from
auditing `usql` for database specific SQL outside its metadata readers, which
found it and found exactly one other thing. See D55.

### Where each one comes from

`ConstraintColumns`. PostgreSQL unnests `conkey` with ordinality and indexes
`confkey` by the same ordinal, which is what keeps a composite key together.
MariaDB, MySQL and the shared model read `key_column_usage`. SQLite unions
three pragmas, matching the three kinds its `Constraints` reports.

`RoutineParameters`. PostgreSQL unnests `proallargtypes`, falling back to
`proargtypes` where a routine has no output parameter. The others read
`parameters`. SQLite has none: a function there is compiled C and SQLite
publishes only how many arguments it takes.

`Views`. PostgreSQL calls `pg_get_viewdef` and lists a materialized view
alongside a plain one, as `psql` does. The others read `views`, except SQLite,
which returns the whole `CREATE VIEW` statement because that is all it stores.

`CurrentSchema`. It is session dependent and it says so. PostgreSQL returns the
first entry of `search_path` that exists. MariaDB and MySQL return the database
in use, and no row at all when the connection named none, which is honest: an
unqualified name resolves nowhere until a `USE` runs. SQLite returns `main`,
which is a constant, because SQLite looks in `temp` first and reports nothing
about that.

`EnumValues`. Of the seven, only PostgreSQL and DuckDB have an enumerated
type. MariaDB and MySQL have an
enum column rather than an enum type, and the labels exist only inside the
`enum('red','green','blue')` text of `COLUMN_TYPE`. Splitting that correctly
needs to track quoting, because a label can contain a comma or an escaped
quote, and no portable SQL does that. `Columns.DataType` returns the text
verbatim, so a caller that knows the product's quoting rules can parse it.
`dbtpl` splits it in Go today and has the same limitation.

`ColumnStats`. It is runtime and it can be stale, and a column never analyzed
has no row. PostgreSQL reads `pg_stats`. MariaDB reads `mysql.column_stats`,
which `ANALYZE TABLE ... PERSISTENT` fills, and derives the distinct count from
the average frequency and the row count. MySQL keeps only a JSON histogram in
`information_schema.COLUMN_STATISTICS`, with no width, no null fraction and no
distinct count, and only where somebody ran `ANALYZE TABLE ... UPDATE
HISTOGRAM`, so it reports the kind unsupported rather than returning rows that
are almost all absent. SQLite's `sqlite_stat1` holds one text string per index
and says nothing about a column's values.

### Two fields rather than kinds

`Column.PrimaryKey` is the case the policy was written around. MySQL has
`COLUMN_KEY` and SQLite has the `pk` column of `pragma_table_xinfo`, so it is
already in the row both select. PostgreSQL needs one more join, to the primary
key index of the relation. Free on two, one join on the third, and it saves
every consumer a second query and a join in Go.

`Function.ID` identifies a routine where the name does not. PostgreSQL returns
the oid, because it allows two functions with one name and different
parameters. Everything else returns the name or the specific name, because
nothing else here overloads. `RoutineParameters` carries the same value, so a
caller writes one join for every database.

## DuckDB

DuckDB answers 21 of the 65. Its catalog is
unusually complete for an embedded database: comments on most objects, real
enumerated types, sequences with their bounds, and a constraint catalog that
names both the columns of a key and the columns they reference.

Like SQLite it has no server, so the release under test is whichever one the Go
driver was built with, there is no container, and the model carries no version
gate. Unlike SQLite its driver needs cgo, which D48 allows in the test module.

### How the catalog is shaped

DuckDB publishes its catalog as table functions named `duckdb_something` rather
than as views, and they behave like tables in a `FROM` clause:

```sql
FROM duckdb_columns() c JOIN duckdb_tables() t ON t.table_oid = c.table_oid
```

Where PostgreSQL carries an array, DuckDB carries a list, and
`unnest(list) WITH ORDINALITY` turns one into rows. That is how the columns of
a constraint, the labels of an enum and the parameters of a function are read,
which is the same shape the PostgreSQL model uses for the same things.

Every catalog function carries an `internal` flag, so the system object rule is
one clause and there is no list of schema names to keep up to date. That is
better than every other model here manages.

### What it answers

Schemas, databases, tables, columns, indexes, constraints, constraint columns,
sequences, views, comments, types, enum values, functions, aggregates, routine
parameters, collations, settings, extensions, the current schema and the
current user.

Two are worth naming. `Comments` gathers comments from five catalog functions,
because DuckDB puts a comment on the object rather than in a catalog of
comments. `Extensions` is a real answer rather than an analogue: a DuckDB
extension is installed and loaded separately, which is closer to a PostgreSQL
extension than anything else here manages.

### The one that looks answerable and is not

`IndexColumns`. `duckdb_indexes` has an `expressions` column that prints as
`[title]`, and its type is `VARCHAR` rather than `VARCHAR[]`. It is a rendering
of a list rather than a list, so `unnest` refuses it, and splitting the text on
a comma breaks the moment an index is on an expression containing one, such as
`concat(a, b)`.

`duckdb_constraints.constraint_column_names` is a real `VARCHAR[]`, which is
why `ConstraintColumns` works and this does not. The columns of a primary key
or a unique constraint are therefore readable and the columns of an index
someone created are not.

### Analogues that were found and rejected

`SUMMARIZE` or `PRAGMA storage_info` for `ColumnStats`. The two reviews split
on this and running it settled it against both. DeepSeek said `storage_info`
provides column statistics: it returns one row per row group per segment with
the statistics as a formatted string, `[Min: 1, Max: 199][Has Null: false]`,
which is prose rather than parts. Gemini said nothing provides them, and missed
`SUMMARIZE`, which returns genuinely good per column statistics including the
minimum, maximum, approximate distinct count, mean and null fraction.

`SUMMARIZE` is rejected for a different reason: it scans the data rather than
reading a catalog, and it takes one statement per table. That fails the cost
test in D47 and the one statement rule in D33. PostgreSQL and MariaDB keep
statistics the planner wrote. DuckDB computes them on demand and stores
nothing, so there is no catalog to read.

`duckdb_dependencies` for `ExtensionObjects`. Both reviews rejected it. It
records every catalog dependency, such as a view on a table, rather than what
an extension owns.

An attached PostgreSQL, SQLite or MySQL database for `ForeignTables`,
`ForeignServers` or `ForeignDataWrappers`. `ATTACH` makes the other database a
full catalog rather than a wrapped remote, so it is reported as a `Database`,
which is what it is.

Hive partitioning through `read_parquet()` for `PartitionedTables`. It is a
file layout read at scan time, not a cataloged object.

### A difference worth knowing

DuckDB generates its own constraint names and ignores one given in the DDL. The
fixture writes `CONSTRAINT shipment_region_fk` and DuckDB records
`shipment_country_area_country_area_fkey`. A caller matching constraints across
databases by name will not find them.

### NOT NULL, and why it is filtered here too

DuckDB records a NOT NULL as a row in `duckdb_constraints`, exactly as
PostgreSQL 18 does, and `Constraints` and `ConstraintColumns` both leave it
out.

D49 filtered it on PostgreSQL for cross release consistency, which does not
apply here because DuckDB has always recorded it. A second reason does:
PostgreSQL never reports a NOT NULL constraint. If DuckDB reports one, the
same schema answers differently by database. `Column.Nullable` carries the fact
in both.

DeepSeek argued the other way, that `duckdb_constraints` is DuckDB's own
catalog and hiding a row loses information. The information is not lost. The
constraint name is, which D49 already records as a known gap.

### What it has none of

Everything that needs more than one user or more than one process. No roles, no
privileges, no triggers, no tablespaces. No casts, domains, operators,
procedural languages, large objects, event triggers, text search objects or
replication.

### The describe fields (D208)

| Field or kind | What DuckDB fills |
| --- | --- |
| `Table.Persistence` | `temporary` or `permanent`. NULL for a view |
| `Table.Rows` | `estimated_size` of `duckdb_tables`, which is an estimate. NULL for a view |
| `Table.Size` | NULL. The size is in `pragma_storage_info`, which takes the table as an argument and cannot be joined for every table |
| `Table.Owner`, `AccessMethod`, `Options` | NULL. DuckDB has no users, no access method and no table options |
| `Index.Persistence`, `Definition`, `Using` | `temporary` for an index in the temp database, the `sql` of `duckdb_indexes`, and `art` |
| `Index.Predicate`, `ConstraintType`, `Size` | NULL. DuckDB refuses a partial index, an index of a key is not in `duckdb_indexes`, and no view gives a size |
| `Column.Compression` | NULL. DuckDB chooses a compression for each segment, and `pragma_storage_info` has it |
| `Constraint.Enforced` | NULL. DuckDB checks every constraint, and no catalog column says it |
| `Function.Prosrc` | `macro_definition`, the same text as `source` |
| `NotNulls` | new. Each NOT NULL is a row of `duckdb_constraints` with a name, such as `author_name_not_null`. The `constraints` query still leaves it out (D49) |
| `Partitions`, `Policies`, `Inherits` | DuckDB has none of the three |

## Microsoft SQL Server

SQL Server answers 34 of the 65, more than SAP HANA and fewer than only
PostgreSQL and CockroachDB, which shares the PostgreSQL model. It is the only
one of its own model with roles, privileges, tablespaces and DDL triggers, and
the only one that keeps comments in a catalog of their own rather than on the
object.

It is tested on every major release that runs on Linux: 2017, 2019, 2022 and
2025, all four on every push. 2016 and older have no container, so they are
provisioned in a Windows virtual machine instead and are Verified rather than
Tested. D54 says what a Linux release claims and D57 says what a machine does.

| Release | How | Verified against |
| --- | --- | --- |
| 2017, 2019, 2022, 2025 | Linux container, every push | 14.0.3550.4, 15.0.4490.9, 16.0.4295.3, 17.0.5005.3 |
| 2016 | Windows Server 2016 machine | 13.0.5026.0 SP2 Express, on 2026-09-25 |
| 2014 | Windows Server 2012 R2 machine | 12.0.2000.8 RTM Express, on 2026-09-25 |
| 2012 | Windows Server 2012 R2 machine | 11.0.7001.0 SP4 Express, on 2026-09-25 |
| 2008 R2 | Windows Server 2008 R2 machine | 10.50.4000.0 SP2 Express, on 2026-09-25 |

The gates show up as a clean progression, and every step of it was checked
against a real server rather than a version set:

| Release | Answers | Refused as too old |
| --- | --- | --- |
| 2016 and newer | 34 | none |
| 2014, 2012 | 32 | `ForeignTables`, `Policies` |
| 2008 R2 | 30 | `ForeignTables`, `Policies`, `Sequences`, `ColumnStats` |

`ForeignTables` reads `sys.external_tables`, and `Policies` reads
`sys.security_policies`, which both arrived in 2016. `Sequences`
and `ColumnStats` read `sys.sequences` and `sys.dm_db_stats_properties`, which
arrived in 2012. Each is refused with `ErrVersionTooOld` rather than returning
nothing, which is the whole point of the gate.

The machines earned their cost immediately. Every gate in this model sits below
2017, so until one ran, the old branch of each was checked only by resolving a
statement against a version set with no server. Running 2012 and 2014 found
three faults that no container can find.

The fixture tore nothing down. Every teardown statement used `DROP ... IF
EXISTS`, which arrived in 2016, so on 2014 and older every drop was a syntax
error and the next test met a schema that was already there.

The display line printed the build twice on a release with no service pack.
`@@VERSION` reads `Microsoft SQL Server 2014 - 12.0.2000.8 (X64)`, where the
first parenthesis comes after the build rather than after the year, so cutting
there kept too much. Every release with a Linux container ships a cumulative
update and names it, so the shorter form never appeared.

`ColumnStats` was supported and had nothing to report. The query gates at 2012
and the fixture step that creates the statistics was gated at 2016, following a
comment that had the release wrong. The two gates disagreed, and only a server
between them can show it.

2008 R2 then found a fourth, of the same family. The fixture created a sequence
unconditionally, and `CREATE SEQUENCE` arrived in 2012, so the whole fixture
failed before any query ran. Both the create and the drop are gated at 2012
now. Guarding the drop with `IF OBJECT_ID(...) IS NOT NULL` is not enough,
because 2008 R2 rejects the word `SEQUENCE` when it parses the batch, whatever
the condition around it says.

The count is four faults from four machines, all of them in the part of the
model that no container can reach.

### The describe fields of D206

Every field below is read by one statement and was run on 2017, 2019, 2022 and
2025. D206 holds the audit, the meaning of each field, the cost and the
differences between releases.

| Kind | Field | Filled from | NULL or false when |
| --- | --- | --- | --- |
| Tables | `Owner` | `sys.tables.principal_id`, or the owner of the schema | never |
| Tables | `Persistence` | temporary in tempdb, unlogged for a schema only memory optimized table | never |
| Tables | `AccessMethod` | `sys.indexes.type_desc`: heap, clustered, clustered columnstore | a view and an external table |
| Tables | `Size` | `sys.allocation_units` through `sys.partitions` and `sys.internal_partitions`, in bytes | a view, a memory optimized table, and below 2012 |
| Tables | `Rows` | `sys.partitions.rows`, summed | a view and a memory optimized table |
| Tables | `Options` | lock escalation and data compression | none set |
| Tables | `RowSecurity`, `RowSecurityForced` | an enabled security policy with a predicate | below 2016. A reader that cannot see the policy reads false |
| Indexes | `Persistence`, `Size`, `Predicate`, `Valid`, `Clustered`, `Options` | `sys.indexes` and the allocation units | `Predicate` for an index that is not filtered. `Size` for a disabled index |
| Indexes | `Deferrable`, `InitiallyDeferred`, `ConstraintType` | false, false and p or u for the index of a key constraint | any other index |
| IndexColumns | `Include` | `is_included_column` | never |
| Constraints | `Enforced` | `NOT is_disabled` | never |
| Sequences | `CacheSize` | `sys.sequences.cache_size` | a sequence with no cache |
| PartitionedTables | `Owner`, `AccessMethod`, `DirectSize`, `TotalSize` | as for Tables | as for Tables |
| Partitions | the whole kind | `sys.partitions` and the partition function | never. A partition has a number and no name |
| Policies | the whole kind, with `Enabled` | `sys.security_policies` and `sys.security_predicates`. `Enabled` is `is_enabled` of the policy, and a policy with STATE = OFF is a row with `Enabled` false (D211). A predicate has no state of its own | below 2016 |

No source exists for `Column.Storage`, `Compression` and `StatsTarget`,
`Index.Owner`, `ReplicaIdentity`, `Definition` and `Using`, `Function.Prosrc`,
`NotNulls` and `Inherits`. A column has no storage mode, and a compression is a
property of a partition and appears in `Options`. A SQL Server index has no
owner and no function writes its statement. A NOT NULL has no name, and tables
do not inherit.

### The sys schema, not information_schema

SQL Server ships both, and the `sys` views carry what `information_schema`
cannot: whether an index is unique, what a foreign key points at, where a table
lives, and the identity of every object. Microsoft's own documentation says to
prefer them, and D9 says to prefer a native catalog anyway.

### What it answers

Schemas, databases, tables, columns, indexes, index columns, constraints,
constraint columns, sequences, views, triggers, event triggers, comments,
types, domains, collations, functions, aggregates, routine parameters, roles,
role grants, privileges, tablespaces, partitioned tables, foreign servers, user
mappings, foreign tables, column statistics, extended statistics, settings, the
current schema and the current user. D206 added the partitions and the row
level security policies.

Four are worth naming. `Comments` reads `sys.extended_properties` for the
`MS_Description` property, which is the convention every SQL Server tool uses
and the closest thing the product has to `COMMENT ON`. `ForeignServers` and
`UserMappings` read `sys.servers` and `sys.linked_logins`, because a linked
server is what SQL Server has instead of a foreign server. `Tablespaces` reads
the filegroups, which is a genuine match rather than an analogue: a filegroup
is where a table's pages live and that is what the question asks.

### Visibility rather than refusal

A SQL Server catalog view shows the caller what the caller is allowed to see and returns
fewer rows otherwise. It does not refuse.

Three queries here depend on a privilege. `ColumnStats` reads
`sys.dm_db_stats_properties` and needs `VIEW STATISTICS`. `UserMappings` reads
`sys.linked_logins` and needs a server level permission. `ForeignTables` reads
`sys.external_tables`, which exists in every install and holds nothing until
PolyBase is configured. All three were run as a user holding `VIEW DEFINITION`
alone, and all three returned an empty result rather than an error.

That is worth knowing because it is a third behavior. PostgreSQL shows a
caller everything, MariaDB refuses outright, and SQL Server quietly narrows the
answer. A consumer that treats an empty result as "there are none" is wrong on
SQL Server in a way it is not wrong on PostgreSQL.

### The gates, all of which sit below the oldest testable release

`sys.sequences` and `sys.dm_db_stats_properties` arrived in 2012.
`sys.external_tables`, `sys.tables.is_external` and `sys.tables.temporal_type`
arrived in 2016. Every one of those is below the 2017 floor, so CI reaches the
new branch of each gate and never the old one.

`models/sqlserver/version_test.go` holds the old branch instead. It resolves
each statement against a version set without a server and checks that a gated
query refuses below the release that added its view, that the padded
alternative never names a column an older release has not got, and that no
other query quietly depends on a release. See D54.

One thing the model avoids on purpose. `STRING_AGG` arrived in 2017 and is
safe at this floor, and the model uses `STUFF(... FOR XML PATH(''))`
instead, which works from 2005. The floor is a container fact and the queries
do not have to inherit it.

### What it has none of

No enumerated type, so nothing for `EnumValues` to list. No operator catalog
and nothing a user can create, which rules out `Operators`,
`OperatorClasses`, `OperatorFamilies`, `Casts` and `Conversions`. No procedural
language catalog: T-SQL is the language and a CLR assembly is not one, which
rules out `Languages`. No text search objects of the shape `psql` names and no
logical replication, which rules out `TextSearchConfigs`,
`TextSearchParsers` and their relatives, `Publications` and `Subscriptions`.

### Analogues that were found and rejected

Hard rule 14 says to ask several models and then run the answer against a real
server. These were named, checked on SQL Server 2022, and left unsupported.

`Extensions` and `ExtensionObjects` from `sys.assemblies`,
`sys.assembly_modules` and `sys.assembly_types`. A PostgreSQL extension is a
versioned bundle of catalog objects that one statement installs and one removes.
A CLR assembly is compiled .NET code that a later `CREATE FUNCTION` can point
at, which is not the same thing. On a stock 2022 the view holds exactly one
row, `Microsoft.SqlServer.Types`, and `clr enabled` is 0, so the feature is off
by default and Azure SQL Database forbids it outright. Reporting one disabled
system assembly as the extensions of the database is worse than saying there
are none.

`DefaultACLs` from `sys.database_permissions` where `class = 3`. A default ACL
says what will be granted on objects created later. A schema scoped permission
is a grant that is already in effect and that new objects inherit at runtime.
The two produce a similar result and they are different facts, and the rows are
already reported by `Privileges`. On a stock 2022 the count is 0.

`RoleSettings` from `sys.server_principals` and `sys.database_principals`.
Those carry `default_database_name` and `default_language_name` and nothing
else. A role setting in PostgreSQL is an arbitrary configuration parameter
attached to a role, and two fixed columns are not that.

`ForeignDataWrappers`. A linked server names an OLE DB provider, and the
providers are COM components registered with the operating system. They are
enumerated by an extended stored procedure and not by any catalog view, so
there is nothing to read. `ForeignServers` already answers the question the
linked server itself asks.

`AccessMethods`. SQL Server's index kinds are engine features rather than
catalog rows. They appear as a `type_desc` on `sys.indexes` and there is no
catalog of the methods themselves, and nothing can add one.

`LargeObjects`. Every binary value in SQL Server is a column value. FILESTREAM
and FileTable put the bytes on disk and keep them addressed by the row, so
there is no object with an identity and an owner of its own to list.

## Oracle

Oracle answers 29 of the 65. Every one is verified on six releases, apart from
the three that D206 added, which were run on 11g, 21c, 23ai and 26ai.

It needs no Windows and no virtual machine, which is the opposite of SQL
Server. The free Express images reach back to 11g Release 2 from 2010, so every
release runs in an ordinary container. See D57 for the SQL Server contrast and
`container/oracle.go` for the list.

### What it answers

Schemas, tables, columns, indexes, index columns, constraints, constraint
columns, sequences, views, the current schema and the current user. Then
comments, triggers, event triggers, functions, aggregates, routine parameters,
types, domains, operators, privileges, column statistics, extended
statistics, partitioned tables, foreign servers and foreign tables.

Verified on 11g, 18c, 19c, 21c, 23ai and 26ai. Every release runs 25 of them
and each returns the number of columns it declares. `Domains` is the
twenty-sixth and needs 23ai, where the SQL domain and `ALL_DOMAINS` arrived,
so 23ai and 26ai run all 26 and the four older releases report that the server
is too old. D206 added `Partitions`, `Policies` and `NotNulls`. `NotNulls`
needs 12c, so 11g runs 27, 12c to 21c run 28 and 23ai and 26ai run all 29.
Express 11g has no partitioning and no virtual private database, so `Partitions`
and `Policies` answer no rows there.

### The describe fields of D206

D206 holds the audit, the meaning of each field, the cost and the differences
between releases. 11g, 21c, 23ai and 26ai ran. 18c and 19c did not.

| Kind | Field | Filled from | NULL or false when |
| --- | --- | --- | --- |
| Tables | `Owner` | the schema, because a table belongs to the user | never |
| Tables | `Persistence` | `temporary` | never |
| Tables | `AccessMethod` | `iot_type` and `cluster_name`: heap, index organized, cluster | a view |
| Tables | `Size` | `user_segments`, summed | a table of another schema than the connected user, and a view |
| Tables | `Rows` | `all_tables.num_rows` | a table that was never analyzed, and a view |
| Tables | `Options` | on_commit, nologging, pctfree, compress, row_movement, read_only | none set |
| Tables | `RowSecurity`, `RowSecurityForced` | an enabled policy in `all_policies` | a reader that cannot see the policy reads false |
| Indexes | `Owner`, `Persistence`, `Size`, `Valid`, `Clustered`, `Options` | `all_indexes` and `user_segments` | `Size` for an index of another schema. `Valid` for a partitioned index |
| Indexes | `Deferrable`, `InitiallyDeferred`, `ConstraintType` | the primary key or unique constraint that owns the index | any other index |
| Constraints | `Enforced` | `status = 'ENABLED'` | never |
| Sequences | `CacheSize` | `all_sequences.cache_size` | never |
| Partitions | the whole kind | `all_tab_partitions` with `high_value` | a table with no partitions. Subpartitions are not rows |
| Policies | the whole kind, with `Enabled` | `all_policies`. The function is the expression. `Enabled` is the column `ENABLE`, which is YES or NO, and a disabled policy is a row with `Enabled` false (D211) | never |
| NotNulls | the whole kind | `all_constraints` of type C with `"COLUMN" IS NOT NULL` | refused below 12c |

No source exists for `Column.Storage`, `Compression` and `StatsTarget`,
`Index.Predicate`, `Definition` and `Using`, `Function.Prosrc` and `Inherits`.
`DBA_SEGMENTS` and `DBA_POLICIES` need a role and are not read, so a size is
known for the connected user only.

### Which user the tests run as

Every Oracle service here names a pluggable database, so the fixture user is a
local user on every release that has the concept. A local user authenticates
against one pluggable database and has nothing at the container level, and it
is the Oracle equivalent of a SQL Server contained database user. A user
created in the container root is a common user instead, which exists in the
root and in every pluggable database at once, and that is the equivalent of a
SQL Server server login.

This was wrong until it was measured. XE answered as `CDB$ROOT` and FREE
likewise, so 18c, 21c, 23ai and 26ai built the fixture as a common user while
only 19c built it as a local one. Nobody had chosen that, and the queries were
being validated mostly against a principal a consumer does not use. The
gvenzl images set `common_user_prefix` to empty, which is why a plainly named
user in the root was accepted there and raised `ORA-65096` on the 19c image.

11g needs none of this. It predates multitenant, so it has no container
database and no `COMMON` column, and `XE` is the instance itself.

Nothing tests a connection to `CDB$ROOT` now. That is a real thing a DBA does
and it is worth a target of its own, named so that a failure reads root rather
than a release. It is not written yet.

### What the conformance test says

Oracle is in `test/testdata/conformance.txt` under `[oracle]`, which D53
requires. Its lines match SQL Server exactly apart from one, and the two
differences from PostgreSQL are both product differences rather than faults:

`recent.book_id` and `recent.title` are `nullable=false`. Oracle carries the
NOT NULL of a base column into a view, and so does SQL Server. PostgreSQL and
SQLite report a view column as nullable whatever it is selected from.

`author_id`, `book_id` and `shipment_id` are `has_default=false`. The Oracle
fixture gives them a plain `NUMBER(10)` primary key, because an identity
column is 12c and later and the fixture has to build on 11g. PostgreSQL uses
`serial`, which is a default.

The canonical projection folds identifier case. Oracle stores an unquoted name
in upper case and everything else here stores it in lower case, and that is a
difference in spelling rather than in structure. See `fold` in
`test/canonical.go`.

Adding Oracle found one fault. `Constraints` returned the fixture's check
constraint and `ConstraintColumns` returned no columns for it, because the
second query dropped every check rather than only the generated NOT NULL ones
that D49 excludes. The rule was written out twice and the two copies came
apart. Both now read `notGeneratedNotNull`.

### ALL_ rather than DBA_ or USER_

Every query reads the `ALL_` views. Both reviews agreed, and the reasoning is
the one this project already knows from `information_schema`: `ALL_` shows the
connected user what it can see and needs no special role, `DBA_` needs
`SELECT_CATALOG_ROLE` that an ordinary application user does not have, and
`USER_` shows only the caller's own schema and has no `OWNER` column at all.

The cost is the same one `information_schema` has. `ALL_` silently drops a row
the caller cannot see, so an unprivileged connection gets a smaller answer
rather than an error.

### A schema is a user

Oracle has no schema object separate from the user that owns it. `Schemas`
reads `ALL_USERS`, the owner of a schema is its own name, and the fixture
creates a user rather than a schema. That also makes the teardown one
statement, because dropping the user cascades to everything it owns.

### Four things the dictionary does that nothing else here does

`LONG` columns. `ALL_TAB_COLUMNS.DATA_DEFAULT`, `ALL_CONSTRAINTS.SEARCH_CONDITION`
and `ALL_VIEWS.TEXT` are all `LONG`, which cannot be joined, compared or
wrapped in a function. They are selected bare. Oracle added `VARCHAR2` twins
for two of them later, `SEARCH_CONDITION_VC` in 12c and `TEXT_VC` in 18c, and
those are gated so an older release reads the `LONG`.

No boolean before 23ai. Nullability is a `Y` or an `N`, uniqueness is the word
`UNIQUE`, and a cycling sequence is a `Y`. Every one is translated with a
`CASE`.

One letter constraint kinds. `P`, `U`, `R` and `C`, where `R` is a foreign key
because Oracle calls it referential. `C` covers a check and a `NOT NULL`
together, and D49 says a `NOT NULL` is not a constraint row, so the generated
ones are filtered out.

A system schema flag, from 18c. `all_users.oracle_maintained` says whether
Oracle created the user, and it is the authority where it exists: it finds
OJVMSYS on 19c, DGPDB_INT on 21c, and BAASSYS, GGSHAREDCAP and VECSYS on 23ai,
none of which a written list had. The written list stays for 11g, which has no
such column, and for PDBADMIN, which the flag calls a person's account from
19c because the script that creates the pluggable database creates it. So the
test is the list on 11g and the list and the flag from 18c.

### Releases, and what separates them

| Release | Version it reports | Why it is here |
| --- | --- | --- |
| 11g | 11.2.0.2.0 | the one release before multitenant: no identity column, no `CDB_` views, no `CON_ID`, 30 byte identifiers |
| 18c | 18.0.0.0.0 | where `version_full` and `TEXT_VC` arrived |
| 19c | 19.0.0.0.0 | the long term release most installations run |
| 21c | 21.0.0.0.0 | native JSON as a column type |
| 23ai | 23.0.0.0.0 | domains, and the vector and boolean types |
| 26ai | 23.26.3.0.0 | 23.26, not 26: a dozen more `ALL_` views than 23ai |

The version comes from the banner, which is the one source every release has.
`product_component_version.version_full` is more precise and does not exist
before 18c, and `v$version.banner_full` likewise, so both are a parse error on
11g. The banner also carries the name Oracle sells the release under, which
nothing else does, and its number separates 23ai from 26ai.

### 11g reads its whole catalog slowly

11g XE reads four queries slowly when the system objects are included:
tables, types, privileges and column_stats. The catalog holds 4,818 tables
with the system objects, and 18c reads the same queries in seconds. The plans
were read on 2026-09-30, on a fresh server with nothing else running (D150).

Two views are the cost. `all_objects` takes 196 seconds to count and
`all_types` 126, alone, while `dba_objects` takes 0.1 and every other view
the queries read takes about a second. Each row of the two views runs a
chain of privilege checks against the fixed tables `X$KZSPR` and `X$KZSRO`,
and an ordinary user with only `CREATE SESSION` waits as long as SYSTEM.
The dictionary statistics in the image date from 2011, and fixed objects
had none, but gathering both changed neither time. tables reads
`all_objects` and types reads `all_types`, so both stay slow on 11g.

The other two were slow because of what they joined, and both are fixed.
privileges looked up the type of each object in `all_objects`. From 12c
`all_tab_privs` names the type itself, so the lookup runs only where it says
UNKNOWN, and 11g, which has no such column, keeps it. column_stats joined
`all_tables` for the row count, which took 304 seconds over every schema. A
lookup for each row takes 41 seconds on 11g, 0.8 on 26ai, and 0.3 for one
schema, where the join took 0.4.

The scan test in the `test` module still reads 11g without the system
objects, because tables and types alone take more than five minutes with
them. Every query then answers, and filtered to the user's schemas the
privilege checks run for few rows.

### The D43 pass, and what it found

Eleven of 55 was too few for a dictionary this rich, so both models were asked
to sort the other 44. They disagreed in a way that proves the rule: DeepSeek
marked `ForeignServers` and `ForeignTables` absent, and Gemini named
`ALL_DB_LINKS` and `ALL_EXTERNAL_TABLES` for them. A database link is exactly
what a foreign server is, and `usql`'s own Oracle reader already queries it.

The pass produced nineteen leads. D43 says to run each against a real server
before believing it, and running them removed five. Fourteen shipped, which
took Oracle from 11 to 25.

| Question | Reads | Note |
| --- | --- | --- |
| `Comments` | `ALL_TAB_COMMENTS`, `ALL_COL_COMMENTS` | the two are unioned, and a column is named table.column |
| `Triggers` | `ALL_TRIGGERS` | where `base_object_type` is a table or a view |
| `EventTriggers` | `ALL_TRIGGERS` | where `base_object_type` is `SCHEMA` or `DATABASE` |
| `Functions` | `ALL_PROCEDURES` | where `aggregate` is `NO` |
| `Aggregates` | `ALL_PROCEDURES` | where `aggregate` is `YES` |
| `RoutineParameters` | `ALL_ARGUMENTS` | |
| `Types` | `ALL_TYPES` | the element type comes from `ALL_COLL_TYPES` |
| `Domains` | `ALL_DOMAINS`, `ALL_DOMAIN_COLS` | 23ai, so gated |
| `Privileges` | `ALL_TAB_PRIVS` | one row per grant, folded into one list per object |
| `ColumnStats` | `ALL_TAB_COL_STATISTICS` | |
| `PartitionedTables` | `ALL_PART_TABLES`, `ALL_PART_KEY_COLUMNS` | |
| `Operators` | `ALL_OPERATORS`, `ALL_OPBINDINGS`, `ALL_OPARGUMENTS` | one row per binding |
| `ForeignServers` | `ALL_DB_LINKS` | a database link |
| `ForeignTables` | `ALL_EXTERNAL_TABLES` | |

### The five leads a real server disproved

D43 exists for this. Three of the views the models named do not exist on any
release here, and checking took one query:

| Lead | 11g | 19c | 26ai |
| --- | --- | --- | --- |
| `ALL_TABLESPACES` | no | no | no |
| `ALL_COLLATIONS` | no | no | no |
| `ALL_PDBS` | no | no | no |

`Tablespaces` is in `USER_TABLESPACES` and `DBA_TABLESPACES` only, which D60
decided against. `Collations` was said to have arrived in 12.2, and it did
not: 26ai has
no such view either. `Databases` has the same problem, and `ALL_PDBS` never
existed under that name.

Two more exist and were rejected on reading them:

`LargeObjects` from `ALL_LOBS`. The view describes the storage of a LOB column
of a table, not an object with an identity of its own. There is no id to
return for `LargeObject.OID`, because Oracle has no standalone large object.
A stretch, and D43 says leave a stretch unsupported.

`ExtendedStats` from `ALL_STAT_EXTENSIONS` was left out at first, because
the view holds an extension's expression and ExtendedStat had no field for
one. `ExtendedStat.Definition` holds it now (D147), and the lead was measured
again on 2026-09-30 and shipped (D149). An extension is a column group or an
expression that `DBMS_STATS.CREATE_EXTENDED_STATS` makes, and its statistics
are those of a hidden column named for it. The optimizer always counts its
distinct values, which is ndistinct, and a frequency or top frequency
histogram on the hidden column is a list of the most common values, which is
mcv. Oracle has no functional dependency statistic. Whether a histogram
exists depends on how the statistics were gathered, where PostgreSQL declares
the kind when it makes the object. The fixture makes a column group on
author.

`Settings` from `V$PARAMETER` is a lead that still stands and is not written.

### Analogues the pass rejected

`AccessMethods` from `ALL_INDEXTYPES` and `OperatorClasses` from
`ALL_INDEXTYPE_OPERATORS`. An Oracle indextype is a user written access method
for a domain index, which is a narrower thing than a PostgreSQL access method,
and neither view describes the built in ones. A stretch, and D43 says leave a
stretch unsupported.

`Publications` and `Subscriptions` from `ALL_PUBLISHED_COLUMNS` and
`ALL_SUBSCRIPTIONS`. Those belong to Change Data Capture, which Oracle
deprecated, and they describe something other than logical replication.

### What Oracle has none of

No enumerated type, so nothing for `EnumValues`. No procedural language
catalog, no cast catalog, no conversion catalog, no operator class or family,
no text search objects of the shape `psql` names, and no extension.

`Roles` and `RoleGrants` are a different case and are not absent. Oracle has
both, in `DBA_ROLES` and `DBA_ROLE_PRIVS`, and there is no `ALL_` equivalent:
an ordinary user sees only its own, through `USER_ROLE_PRIVS` and
`SESSION_ROLES`. Answering them means reading `DBA_`, which needs
`SELECT_CATALOG_ROLE`, and D60 decided not to reach for it. Neither query is
written.

`Databases` is the same shape of problem. Oracle has one database per
instance, and the nearest list is the pluggable databases, which no `ALL_`
view carries: `ALL_PDBS` does not exist on any release here. `DBA_PDBS` and
`V$DATABASE` do, and D60 decided against reading either.

## CockroachDB

`models/cockroachdb` answers 54 of the 65, on 24.3.36, 26.2.7 and 26.3.2. It
was measured on 2026-09-29, with pgx, which is the driver dburl opens for
`cockroachdb://`. CockroachDB imitates PostgreSQL's catalog, so 47 of its
statements are the postgres model's, shared with `Query.Share`. The version
set's main version is the PostgreSQL release that CockroachDB claims, 13.0.0
on 24.3 and 26.2 and 18.0.0 on 26.3, so a shared statement takes the fragments
of that release. CockroachDB's own release is under the key `cockroachdb`.
See D123.

The fields of D198 run on CockroachDB 26.2.7 and 26.3.2 without a fault. The
Tables, Columns, Indexes and Functions statements are shared. `size` is NULL
before 26.3, which has no `pg_table_size`. The fixture leaves out the steps
that build the objects the new fields read, so their values are not checked
there.

The fields of D201 run there too. `Extension.DefaultVersion` is NULL, because
there is no `pg_available_extensions()`. The `Visible` flags of the operator
classes and families are NULL, because there is no `pg_opclass_is_visible` or
`pg_opfamily_is_visible`. `OperatorFamily.AppliesTo` keeps the order by type
name, because there is no `ctid`. `pg_table_is_visible`, `pg_get_indexdef` and
`pg_get_constraintdef` exist, so the index text, the constraint fields and the
flags of tables are filled. The four kinds of D201 are not shared.

Seven statements are its own, because the postgres model's call a function or
a type that CockroachDB lacks:

| Query | What differs |
| --- | --- |
| databases | `size` is NULL before 26.3, which has no `pg_database_size` or `pg_size_pretty` |
| tablespaces | `location` and `size` are always NULL. CockroachDB keeps no tablespace on a path, and `pg_tablespace` holds `pg_default` and `pg_global` for compatibility |
| triggers | `definition` is NULL before 26.2, which has no `pg_get_triggerdef`. 24.3 accepts `CREATE TRIGGER` and records nothing in `pg_trigger` or `information_schema.triggers`, so it answers no rows |
| index_columns | `descending` reads bit 1 of `indoption`, because there is no `pg_index_column_has_property` |
| extension_objects | `description` is always NULL, because there is no `pg_describe_object`. `pg_extension` is empty, and `CREATE EXTENSION` does nothing |
| operator_family_operators | the operator is written from `pg_operator`, because there is no `regoperator` type. `pg_amop` is empty |
| extended_stats | `definition` names the columns from `stxkeys`, because there is no `pg_get_statisticsobjdef_columns`. CockroachDB has no statistics on an expression, so the columns are the whole definition (D147) |

`column_stats` is not answered. CockroachDB keeps `pg_stats` empty even after
ANALYZE, so the shared statement says that no column has statistics,
which is false. Its statistics are in `SHOW STATISTICS FOR TABLE`, which names
the table in the statement rather than taking it as a bind parameter.
`system.table_statistics` holds them too, and 26.3 refuses to read it:
"Access to crdb_internal and system is restricted".

`text_search_config_maps` is not answered. CockroachDB has no
`ts_token_type`, which names the kind of token a mapping is for, and it keeps
`pg_ts_config` empty, so there is no configuration to map (D147).

A parameter declared `integer` reads as `bigint`, because CockroachDB makes
`integer` 64 bits. The fixture builds 29 of the PostgreSQL fixture's 36 steps
on 26.3, and 26 on 24.3 and 26.2, which have no domain and cannot comment on
a sequence or a function. It leaves out the revoke, the two partitioning steps,
the publication, the statistics on an expression, the text search
configuration and the collation with rules, and `models/cockroachdb/fixture`
says why. The
conformance report is line for line PostgreSQL's on all three releases.

Parity finds what PostgreSQL finds, with one difference by release. From 26.3
a principal that is not allowed to connect to a database reads "no access" as its size.

Gemini and DeepSeek were asked about each gap, as hard rule 14 requires, on
2026-09-29. Both called tablespace paths, extensions and operator families
absent from CockroachDB. DeepSeek named `crdb_internal.column_statistics`,
`crdb_internal.table_sizes`, `crdb_internal.create_statements` and a flag for
system schemas. None of them exists, or 26.3 refuses to read it, as measured.
Gemini's trigger statement from `information_schema.triggers` has nothing to
read on 24.3.

With `with_system` off, the shared statements hide `pg_*` and
`information_schema`, as `psql` does, and not `crdb_internal`. So `tables`
lists the 110 to 117 virtual tables of `crdb_internal`. No column of
`pg_namespace` or `pg_class` marks a schema that CockroachDB keeps for itself,
and only its name does. That is what `psql` shows on CockroachDB, which hard
rule 2 follows, and D128 keeps it.

## CrateDB

`models/cratedb` answers 27 of the 65 on 6.4.5 and 26 on 6.3.7, which has no
`information_schema.collations`. It was measured on 2026-09-29 with pgx, on
the PostgreSQL port, which is what dburl opens for `cratedb://`. The main
version is the PostgreSQL release that CrateDB claims, 14.0 on both, and
CrateDB's own release is under the key `cratedb`. See D123.

CrateDB speaks PostgreSQL's protocol and keeps a catalog of its own. Its
`pg_catalog` holds part of PostgreSQL's, and many of the functions that the
postgres model calls are absent. So only 3 statements are shared: settings,
role grants and the current user. The other 24 read `information_schema`,
`pg_catalog` and `sys` directly.

### What each answer lacks

| Query | What differs |
| --- | --- |
| schemas, current_schema | `owner` is empty. CrateDB keeps no owner for a schema, and `pg_namespace` says "unknown (OID=0)" |
| tables, views, privileges | a foreign table has the type `foreign table`. There is no comment on anything, because CrateDB has no COMMENT statement |
| columns | a generated column reads `s`, as a stored one does on PostgreSQL. `identity` is always empty. The column of a foreign table has the ordinal -1, which is what CrateDB reports |
| indexes, index_columns | the only index is the one behind each primary key, and CrateDB reports it as not unique. A full text index is in no catalog, only in SHOW CREATE TABLE. `type` is empty, because `pg_am` is empty |
| constraints | `definition` is NULL on 6.3, which has no `pg_get_constraintdef` and keeps a check expression in no catalog. There is no foreign key and no unique constraint on any release |
| constraint_columns | a primary key only. A check constraint has no row in `key_column_usage` |
| partitioned_tables | the strategy is `LIST`, because a partition holds one value of each partition column |
| functions | a JavaScript function, from `information_schema.routines`. `id` is `specific_name`, which holds the argument types. `volatility` is `immutable` for a deterministic function and `volatile` for the rest. `owner` is NULL, and `security` and `parallel` are empty |
| types | every type is built in and in `pg_catalog`, so the query answers only with `with_system` |
| collations | from 6.4. CrateDB has one collation, in `pg_catalog`, so it too answers only with `with_system` |
| roles | from `pg_roles`, because a user who is not a superuser is refused `sys.users` and `sys.roles`. `create_db` and `bypass_rls` are false, because CrateDB has no database to create and no row security |
| privileges | from `sys.privileges`, a grant on a table as `grantee=type/grantor`, with `denied` after a DENY. A grant on a schema or on the cluster is not a privilege on the table and is not shown |
| databases | CrateDB has one database, `crate`. `owner` is empty, and `size` is NULL, because a size is in `sys.shards`, which a user who is not a superuser cannot read |
| foreign_servers, foreign_tables, user_mappings | the options are aggregated from the three `*_options` views. The only wrapper is `jdbc` |
| publications | `truncate` and `via_root` are false, because CrateDB replicates neither |
| subscriptions | the fixture builds none. A subscription connects to another cluster, and the tests start only one |

### What it does not answer

CrateDB has none of these: tablespaces, access methods, conversions, casts,
large objects, event triggers, domains, operators, role settings, default
privileges, extensions and the objects in them, extended statistics,
comments, triggers, sequences, enum values, and the four kinds of operator
class and family. `pg_tablespace`, `pg_am`, `pg_enum`, `pg_description` and
`pg_event_trigger` are there and empty.

These are left unanswered, although CrateDB holds something like them:

- Aggregates. `pg_proc` holds 178 of them, with `prokind` a, and each names a
  `pronamespace` that is in no row of `pg_namespace`. An answer must invent
  the schema.
- Languages and foreign data wrappers. JavaScript and `jdbc` are the only
  ones, and no view lists them. A distinct `routine_body` or
  `foreign_data_wrapper_name` lists only the ones in use.
- Routine parameters. There is no `information_schema.parameters`, and a
  JavaScript function is not in `pg_proc`. Only `specific_name` holds the
  argument types, with no names.
- The five text search kinds. `information_schema.routines` lists analyzers,
  tokenizers, token filters and char filters, which do the work of a text
  search configuration, a parser and a dictionary. An analyzer has no schema,
  and a built-in one records no tokenizer, so a configuration has no
  parser. D131 leaves them out.

### What the fixture builds

`models/cratedb/fixture` builds the five core tables and the view, a
partitioned table with a generated column, a JavaScript function, a role, a
user, a grant and a denial, a publication, a foreign server with a user
mapping and a foreign table, and rows with statistics over them. It builds
every step on both releases. The conformance report has no foreign key, no
unique constraint and no check, for the reasons above, and D53's agreement
count leaves CrateDB out with that reason.

### Which answers depend on who is asking

Parity asks as `crate` and as two users, one with every privilege on the
fixture schema and one with DQL only. Both answer the same way on both
releases. `privileges` is refused, because a user who is not a superuser is
refused the `sys` schema: "Schema 'sys' unknown". `schemas` lists only the
schemas the user has a privilege in. `foreign_servers`, `user_mappings`,
`publications` and `publication_tables` list fewer rows.
`information_schema.role_table_grants` answers for any user, and it lists only
the grants to the user who asks, so it is not a substitute.

### The describe fields (D207)

D198 and D199 gave Tables, Indexes and Columns fields that only PostgreSQL
filled, and added the kind Partitions. D207 measured each one on 6.3.7 and
6.4.5.

| Field or kind | What CrateDB fills |
| --- | --- |
| `Table.Owner` | NULL. `pg_class.relowner` is 0, `pg_tables.tableowner` is empty and `information_schema.views.owner` is empty, so CrateDB 6 records no owner of a relation |
| `Table.Persistence`, `AccessMethod`, `RowSecurity` | NULL. CrateDB has no temporary table, no access method and no row security |
| `Table.Options` | `number_of_shards`, `number_of_replicas`, `clustered_by`, `partitioned_by` and `column_policy` from `information_schema.tables`, joined by a comma and a space. NULL for a view and a foreign table |
| `Table.Size`, `Table.Rows` | NULL. The exact numbers are in `sys.shards`, and an ordinary user is refused the schema `sys`, so a join to it makes `tables` fail for that user. The estimate is `pg_class.reltuples`, which is the count that ANALYZE stored. A join to it costs about 7 ms at 1300 relations, whatever the filter, so a read of one table is three times slower. D47 does not allow that, and the fields are not added |
| `Index.Valid`, `Clustered`, `ReplicaIdentity`, `ConstraintType` | `indisvalid`, `indisclustered` and `indisreplident` of `pg_index`, and `p` for every row, because `pg_index` holds the primary keys only |
| `Index.Owner`, `Size`, `Predicate`, `Definition`, `Using`, `Options` | NULL. `pg_get_indexdef` is not there, and a primary key index has no predicate and no options |
| `Column.Storage`, `Compression`, `StatsTarget` | NULL. CrateDB has none of the three |
| `Constraint.Enforced` | NULL. CrateDB enforces a primary key and a check, but no catalog column says so, and the field is not invented |
| `Partitions` | new. One row for each partition from `information_schema.table_partitions`. `Partition` is the name of the index, `.partitioned.table.ident`. `Bound` is the values of the partition columns as a JSON object, such as `{"year":2025}`, because a table can have several partition columns and PostgreSQL's LIST takes one. `Constraint` is NULL |
| `RoleSettings` | left unanswered. `sys.users.session_settings` is an object, and CrateDB has no way to read the value of a key that the statement does not name, so one statement can return the object as JSON text and nothing closer to `name=value` |
| `NotNulls`, `Policies`, `Inherits`, `Rules` | CrateDB has none of the four |

The three NULL hazards of D197 do not reach CrateDB. It has no INCLUDE column,
and `IndexColumn.Descending` is a real false. It has no default privilege and
no database level setting.

### What a second opinion found

DeepSeek and Gemini were asked about each gap, as hard rule 14 requires, on
2026-09-29. Both called the kinds in the first list absent. Both named
`pg_proc` for aggregates, `information_schema.routines` for the text search
kinds, and the distinct wrapper names in `information_schema.foreign_servers`
for foreign data wrappers. Gemini also named `routine_body` for languages.
Each lead was run against 6.4.5, and each is left unanswered for the reason
written above.

## QuestDB

`models/questdb` answers 11 of the 65 on 9.4.3 and 10.0.1. It was measured on
2026-09-29 with pgx, on the PostgreSQL interface on 8812, which is what
dburl opens for `questdb://` and what usql uses.

### Which catalog it reads

QuestDB's own catalog is a set of functions: `tables()`, `views()`,
`materialized_views()`, `functions()` and `table_columns()`, which takes one
table name as a constant. Its `information_schema` and `pg_catalog` imitate
PostgreSQL's for the clients that read them, and hold a part of the same
facts. The model reads whichever answers the question in one statement, and
shares nothing with the postgres model, whose statements read catalogs and
functions QuestDB does not have.

The version is read from `SELECT build()`. `SHOW server_version` and
`version()` answer PostgreSQL 12.3 on both releases, and only `build()` names
QuestDB's.

### What each answer lacks

| Query | What differs |
| --- | --- |
| schemas, current_schema | QuestDB has no schemas. `pg_namespace` holds `public` and `pg_catalog`, and every table is in `public`. `owner` is empty |
| tables | from `tables()`, where a table, a view and a materialized view each have a kind of one letter. QuestDB lists no system table there |
| columns | from `information_schema.columns`, because `table_columns()` reads one table at a time. The type is the PostgreSQL type the wire protocol sends, so a SYMBOL reads character varying and a DECIMAL numeric. QuestDB counts the position from 0, and the model adds 1. Every column is nullable, and none has a default or is a key, because QuestDB has none of the three |
| views | a view from `views()` and a materialized view from `materialized_views()`, with the text each was created with. Neither is updatable or insertable |
| partitioned_tables | a table or a materialized view partitioned by its designated timestamp. The strategy is RANGE, and the expression names the interval and the column, as YEAR (published) |
| databases | `qdb`, the one database. The encoding is UTF8, which is the only one QuestDB stores text in. There is no owner and no size |
| settings | from `(SHOW PARAMETERS)`, which QuestDB lets a query select from. There is no type, and the context says whether a setting takes effect without a restart |
| functions, aggregates | from `functions()`, one row per signature. Every function is built in, because QuestDB has no CREATE FUNCTION, so both answer only with `with_system`. A signature can be both an aggregate and a window function, so the id carries the kind |

### What it does not answer

QuestDB has none of these: tablespaces, access methods, languages,
conversions, casts, collations, large objects, event triggers, domains,
operators, roles, role settings, role grants, privileges, default privileges,
foreign data wrappers, foreign servers, user mappings, foreign tables,
publications, publication tables, subscriptions, the five text search kinds,
the four kinds of operator class and family, extended statistics, extension
objects, comments, constraints,
constraint columns, triggers, sequences, enum values and column statistics.
The open source edition has no roles or grants, and `pg_roles` is empty.

These are left unanswered, although QuestDB holds something like them:

- Indexes and index columns. A SYMBOL column can have an index, and only
  `table_columns()` says so, one table at a time. `pg_index` is empty, and
  `table_columns()` refuses a column as its argument, so no one statement
  lists them.
- Types. `pg_type` holds the 17 PostgreSQL types the wire protocol maps
  QuestDB's to, and not QuestDB's own, such as SYMBOL and GEOHASH.
- Extensions. `pg_extension` holds one row, which is QuestDB itself.
- Routine parameters. A built in function's arguments are one text in
  `functions()`, with no names. `split_part` takes only a constant index, so
  one statement cannot split them into rows.
- A deduplication key, which DEDUP UPSERT KEYS declares and which keeps rows
  unique by it. It is not a constraint, and only `table_columns()` names its
  columns, one table at a time.

### What the fixture builds

`models/questdb/fixture` builds the five core tables and the view, with book
partitioned by year on its designated timestamp as a WAL table, a
materialized view on book, and an indexed SYMBOL column on author. It builds
every step on both releases. The conformance report has every column
nullable, none a key and no constraint line, and D53's agreement count leaves
QuestDB out with that reason.

### Which answers depend on who is asking

Parity asks as `admin` and as the user of the PostgreSQL interface that can
only read. Every query answers the same way, except that 10.0.1 reports the
reader's own name as the current user. 9.4.3 reports `admin` for the reader
too, which is the one difference between the releases, and it has a parity
section of its own.

### The describe fields (D207)

| Field or kind | What QuestDB fills |
| --- | --- |
| `Table.Rows` | `table_row_count` of `tables()`, a count. NULL for a view, and for a table that has no row count yet, such as a WAL table before it applied its first transaction |
| `Table.Options` | `wal`, `dedup`, `maxUncommittedRows`, `o3MaxLag` in microseconds and `ttl`, as the WITH clause names them. NULL for a view. The designated timestamp and the interval are in `partitioned_tables` |
| `Table.Size` | NULL. `table_storage()` has `diskSize`, and it reads the disk of every table whatever the filter: 45 ms at 1500 tables, and 40 ms for the one table that was asked for, against 3 ms without it. D47 does not allow that |
| `Table.Owner`, `Persistence`, `AccessMethod` | NULL. The open source edition has no owner, and no temporary table |
| `Partitions` | not answered. `table_partitions()` and SHOW PARTITIONS take the table name as a constant, and QuestDB refuses a bind parameter there, so one statement cannot read the partitions of the tables |
| `Index` fields | the kind is not answered, for the reason above |
| `Column.Storage`, `Compression`, `StatsTarget` | NULL. QuestDB has none of the three |

QuestDB has no INCLUDE column, no default privilege and no role setting, so the
three hazards of D197 do not reach it.

### What a second opinion found

Gemini Pro and DeepSeek were asked about each gap, as hard rule 14 requires,
on 2026-09-29. Both called the kinds in the first list absent. Both named
`pg_type` for types and `pg_extension` for extensions, which hold something
else, as above. Gemini named `table_columns()` for indexes, index columns,
constraints and constraint columns, which reads one table at a time, and
string parsing of `functions()` for routine parameters, which one statement
cannot do. Each lead was run against 10.0.1.

## TiDB

`models/tidb` answers 20 of the 65 on 8.5.8, and 19 on 7.5.8 and 8.1.2,
where privileges is too old. It was measured on 2026-09-29 with the mysql
driver, which is what dburl opens for `tidb://` and what usql uses. TiDB
imitates MySQL's information_schema, so 17 of its statements are the mysql
model's, shared with `Query.Share`. The version set's main version is the
MySQL release TiDB claims, 8.0.11 on every release, set under the `mysql`
key too, and TiDB's own release is under the key `tidb`. See D133.

### What differs from MySQL

| Query | What differs |
| --- | --- |
| every query that filters by schema | TiDB spells INFORMATION_SCHEMA and PERFORMANCE_SCHEMA in capitals, compares a schema name with its case, and has METRICS_SCHEMA of its own, with 637 tables. The mysql model's filter carries an alternative for TiDB that names all five as TiDB spells them |
| roles | `conn_limit` is 0 before 8.5, where `mysql.user` has no `max_user_connections` and TiDB has no limit per user |
| settings | from `information_schema.variables_info`, TiDB's own, because TiDB has no `performance_schema.global_variables`. The context is the scope, such as session,global |
| sequences | from `information_schema.sequences`, TiDB's own. A TiDB sequence is always a bigint |
| privileges | from 8.5. Before it `information_schema.TABLE_PRIVILEGES` is empty although `mysql.tables_priv` holds the grant, so the query is too old there |
| constraints | a CHECK constraint is not listed. TiDB accepts one and enforces it only with `tidb_enable_check_constraint`, and the mysql model lists a check only from MySQL 8.0.16, which TiDB does not claim |

### What it does not answer

TiDB has no stored function, procedure or trigger, and no foreign server, so
functions, aggregates, routine parameters, triggers, foreign servers, user
mappings and foreign tables are not answered. `information_schema.ENGINES`
lists only InnoDB, which TiDB writes for compatibility, so access methods is
not answered either, and neither is extensions. The rest are absent, as they
are on MySQL.

Two are left unanswered although TiDB holds something like them:

- Column statistics. `mysql.stats_histograms` holds a distinct count and a
  null count for each column, keyed by a column id that no catalog view
  names. A match by position is wrong after a column is dropped, and
  only `SHOW STATS_HISTOGRAMS` gives the names, which a statement cannot read.
- Extended statistics. `mysql.stats_extended` exists and is empty, because
  extended statistics are experimental and off by default.

### What the fixture builds

`models/tidb/fixture` is the MySQL fixture less the function, the two
procedures, the trigger and the foreign server, with a sequence, a role, a
user and the grants between them added. It builds every step on the three
releases. The conformance report is line for line what MySQL reports, less
the check constraint, which TiDB does not list.

### Which answers depend on who is asking

Parity asks as root, as a user with every privilege on the fixture schema,
and as one that can only read it. Both lesser users are refused roles and
role grants, which read the mysql schema, as they are on MySQL, and see fewer
schemas, databases and privileges. 8.5 refuses a column of `mysql.user` and
`mysql.role_edges`, and 7.5 and 8.1 refuse the whole table, so 8.5 has a
parity section of its own. The current schema differs because the DSN of root
names the database dbmeta, which the lesser users are not allowed to use, so they connect
to the fixture schema.

### What a second opinion found

Gemini and DeepSeek were asked about each gap, as hard rule 14 requires, on
2026-09-29. Both called functions, triggers, routine parameters, foreign
tables and extensions absent, and named `mysql.stats_extended` for extended
statistics and `mysql.stats_histograms` for column statistics, which are
above. Both called enum values derivable from `COLUMN_TYPE`, which the mysql
model does not answer on MySQL either. DeepSeek named `ENGINES` for access
methods, which is a list kept for compatibility.

### The fields that D205 fills

TiDB fills `Table.AccessMethod` (InnoDB, which it writes for compatibility),
`Size`, `Rows` and `Index.Using` from the statements it shares. Size and rows
come from the statistics, so a table that was never analyzed reads 0, and an
index made USING HASH reads BTREE. `Table.Persistence` is NULL, because a
global temporary table is a base table in `TABLES` and a local one is not
listed. `Options` is NULL, because TiDB keeps no row format. `Constraint.Enforced`
is NULL, because TiDB claims MySQL 8.0.11 and has no `ENFORCED` column.
It answers `Partitions` for range, list and hash partitions. It accepts
SUBPARTITION BY and ignores it, so it has no subpartition rows.

## Vitess

`models/vitess` answers 21 of the 65 on 23.0.6 and 24.0.3. It was measured on
2026-09-30 on vttestserver, with the mysql driver, which is what dburl opens
for `vitess://` and what usql uses. vtgate passes a query of
information_schema to the MySQL of one tablet, so 19 of its statements are
the mysql model's, shared with `Query.Share`. The version set's main version
is the MySQL release Vitess claims, 8.4.6 on both releases, set under the
`mysql` key too. Vitess's own release is under the key `vitess`, read from
`@@version_comment`. See D135.

### A schema is a keyspace

A keyspace is stored in one MySQL database for each shard, named
`vt_<keyspace>_<shard>`. information_schema names that database and never the
keyspace, and vtgate refuses that name in a query: a SELECT from
`vt_dbmeta_q_0.t` failed with VT05003, unknown database, and a SELECT from
`dbmeta_q.t` succeeded. So every statement reports the keyspace, which
`mysql.Keyspace` reads from the name of the database, and a filter matches the
keyspace. The current schema is the keyspace too. A test queries every table
of the fixture under the name the model reports. See D135.

### What differs from MySQL

| Query | What differs |
| --- | --- |
| every query that filters by schema | `_vt` holds the state of the tablet. The mysql model's filter carries an alternative for Vitess that adds it to the system schemas |
| every query that names a schema | the keyspace, read from the name of the shard's database, which is what information_schema holds |
| sequences | Vitess's own. A sequence is a table with the comment `vitess_sequence` and the columns id, next_id and cache, which one statement over information_schema lists. The start, the bounds and whether it cycles are not recorded, so they are NULL. The increment is 1, measured with NEXT VALUE. The VSchema names the column a sequence fills, and information_schema does not, so the owner is empty |
| settings, access methods and extensions | the variables, engines and plugins of the tablet's MySQL, which is the MySQL that stores the rows |

### What it does not answer

Roles, role grants and privileges are not answered, although the mysql
model's statements run. They list the accounts of the tablet's MySQL, such as
`vt_dba`, `vt_app` and `vt_repl`, which Vitess uses itself and no client of
vtgate logs in as. vttestserver starts vtcombo with no authentication, so a
Vitess user cannot be measured either. vtgate refuses CREATE TRIGGER,
CREATE FUNCTION and CREATE SERVER, so triggers, aggregates, foreign servers,
user mappings and foreign tables are not answered. The rest are absent, as
they are on MySQL.

### What the fixture builds

`models/vitess/fixture` is the MySQL fixture less the function, the trigger
and the foreign server, with a sequence added. Both procedures are built. It
builds every step on both releases. The conformance report is line for line
what MySQL reports.

### Which answers depend on who is asking

Nothing. vttestserver starts vtcombo with no authentication and no table
rules, so every user is the same principal, and Vitess is exempt from parity
with that reason.

### What a second opinion found

Gemini was asked about each gap, as hard rule 14 requires, on 2026-09-30, and
Gemini Pro about the ones it named. DeepSeek returned nothing twice, because
its reasoning used every token it was given. Gemini named a table with the
comment `vitess_sequence` for sequences, which is above.
`information_schema.INNODB_TABLESPACES` for tablespaces,
`COLUMN_STATISTICS` for column statistics, `mysql.func` for aggregates and
`COLUMN_TYPE` for enum values are what the mysql model leaves unanswered on
MySQL, for the reasons in the MySQL section. It named
`information_schema.TRIGGERS`, which vtgate passes and which is empty,
because vtgate refuses to create a trigger. It named `mysql.user`,
`mysql.role_edges` and the privilege views for roles and privileges, which
are the tablet's accounts, above. It named `_vt.vreplication` for
subscriptions and said that vtgate does not pass the SELECT.

### The fields that D205 fills

vtgate passes the statements to the MySQL of one tablet, so Vitess fills what
MySQL fills: `Table.Persistence`, `AccessMethod`, `Size`, `Rows` and `Options`,
`Index.Using`, `Constraint.Enforced` (the tablet claims 8.4, so it reads
`ENFORCED`), `Function.Prosrc` is not shared because Vitess has no function,
and `Partitions` with `PartitionedTables.DirectSize` and `TotalSize`.
`Partitions` names the keyspace in `Schema` and `PartitionSchema`. A size and a
row count are those of the one tablet that answers, and not the sum over the
shards of a keyspace.

## Databend

`models/databend` answers 20 of the 65 on 1.2.881 and 1.2.948. It was
measured on 2026-09-30 with dbimp's driver, which is what usql's databend
scheme opens. It reads the system database, as the ClickHouse model does,
and the table function show_sequences(). See D140.

### Why not information_schema

Databend's information_schema reports an ordinal position of 1 for every
column, on both releases, and has no view of indexes, constraints,
functions, sequences or roles. system has all of them. system.columns has no
position either, so the model numbers a table's columns in the order
system.columns lists them, which is the order they were declared in. The
fixture test checks that order.

### What differs

| Query | What differs |
| --- | --- |
| schemas and databases | a database is the only namespace, so both read system.databases. A database Databend made has no owner |
| tables | the type is read from the engine: table for FUSE, view, materialized view, system table, and the engine's name and table for any other |
| columns | no primary key, identity or generated column is reported. Databend has no primary key, and system.columns does not say that a column is AUTOINCREMENT or computed |
| indexes and index columns | an inverted, ngram or vector index belongs to a table and enforces nothing. The columns are read from the definition, such as book(a, b). An aggregating index belongs to a query and has no columns |
| constraints and constraint columns | a CHECK is the only constraint, and system.constraints lists its columns |
| functions and aggregates | a function or procedure a user made belongs to no database, so its schema is empty. The functions built into the server are listed with the system objects, in the schema system |
| sequences | from show_sequences(), because system has no table of sequences. A sequence belongs to no database. The start, the bounds and the type are not recorded |
| roles and role grants | a user and a role are both listed. A user or a role that holds account_admin is a superuser. The roles granted to each are one list joined by commas, which the role grants query splits |

### What it does not answer

Privileges are not answered, because show_grants lists the grants of one
role or user at a time, and a statement cannot call it once for each. Routine
parameters are not answered, because a procedure records its arguments as
one text, such as addup(Int32,Int32) RETURN (Int32), and a function records
them as a variant. Databend has no trigger, type, domain, collation,
extension or partitioned table, and the rest are PostgreSQL's alone.

A computed column needs an Enterprise license on 1.2.881. 1.2.948 accepts
one, and system.columns does not mark it as computed on either release.

### What the fixture builds

`models/databend/fixture` builds the core tables, with no key, a CHECK on
book.title, an inverted and an ngram index, a view, a function, a procedure,
a sequence, two roles and a user with the grants between them, and rows in
author with statistics taken by ANALYZE TABLE. The function, the procedure,
the sequence, the roles and the user belong to no database, so the teardown
drops each by name.

### Which answers depend on who is asking

Parity asks as root, as a user whose role holds every privilege on the
fixture's database, and as one whose role can only read it. Both lesser users
are refused system.settings, system.engines, system.statistics and
system.constraints, so settings, access methods, column statistics,
constraints and constraint columns are refused to them, and they see fewer
schemas and databases.

### What a second opinion found

Gemini and Gemini Pro were asked about each gap, as hard rule 14 requires, on
2026-09-30. Both named the column list of system.constraints for constraint
columns and the definition of system.indexes for index columns, which are
above. The rest are stretches and are left out: a stage is a place files are
read from, not a tablespace, a catalog is a source of tables rather than a
server with options, a task runs on a schedule and not on a change, and a
cluster key orders the rows of a table rather than partitioning it.

### The describe fields (D208)

| Field or kind | What Databend fills |
| --- | --- |
| `Table.Owner` | `owner` of `system.tables`. NULL for a view |
| `Table.Persistence` | `transient` or `permanent`. NULL for a view |
| `Table.AccessMethod` | the engine, such as FUSE or VIEW |
| `Table.Size`, `Table.Rows` | `data_compressed_size` and `num_rows`. The indexes are not in the size |
| `Table.Options` | `cluster_by=expressions`. The rest of `table_option` holds the path of the snapshot, which changes at every write, so it is not read |
| `Function.Prosrc` | the `definition` of a function a user made, the same text as `source` |
| `Policies`, `Table.RowSecurity` | NULL and not answered. Databend 1.2.951 has no system table for a row access policy, `system.policy_references` is not there, and SHOW cannot be filtered |
| `Column.Storage`, `Compression`, `StatsTarget`, the other `Index` fields, `Constraint.Enforced` | NULL. There is no source |

## SingleStore

`models/singlestore` answers 23 of the 65 on 9.0 and 9.1. It was measured on
2026-09-30 on the development image, which runs with no license on a machine
with at most 8 cores and 64 GB, through the mysql driver, which is what
dburl's memsql scheme opens and what usql uses. SingleStore imitates MySQL's
information_schema, so 16 of its statements are the mysql model's, shared
with `Query.Share`. The version set's main version is the MySQL release
SingleStore claims, 5.7.32 on both releases, set under the `mysql` key too,
and SingleStore's own release is under the key `memsql`, read from
`@@memsql_version`. See D141.

### What differs from MySQL

| Query | What differs |
| --- | --- |
| every query that filters by schema | memsql and cluster are SingleStore's own schemas. The mysql model's filter carries an alternative for SingleStore that names them |
| constraints | SingleStore reports the primary key of a columnstore table as UNIQUE, with the name PRIMARY. The mysql model's statement carries an alternative that reports it as the primary key |
| index columns | a shard key is listed as an index of the type SHARD, and a key that is also the shard key is listed twice. The mysql model's statement carries an alternative that leaves out the second row |
| settings | from GLOBAL_VARIABLES, because SingleStore has no performance_schema. The context is whether a variable can be set while the server runs |
| roles | from USERS, ROLES and GROUPS, because SingleStore has no mysql database. A user can log in, and a role and a group cannot. A group's member_of is the roles it holds |
| role grants | from GROUPS_ROLES, a role granted to a group. No view says which groups a user is in |
| privileges | from ROLE_PRIVILEGES, because TABLE_PRIVILEGES and SCHEMA_PRIVILEGES are empty although a role holds a grant. A grant to a user is in no view |
| aggregates | from AGGREGATE_FUNCTIONS, a user defined aggregate and the four functions that make it |
| column statistics | from OPTIMIZER_STATISTICS, which ANALYZE TABLE fills. The bounds are only inside the histogram |
| extended statistics | from CORRELATED_COLUMN_STATISTICS, which `ANALYZE TABLE ... CORRELATE COLUMN` fills. A correlation says how strongly one column follows another, which is PostgreSQL's functional dependency, so the kind is f. It has no name (D149) |

### What it does not answer

SingleStore refuses a foreign key, a trigger, a foreign server and PARTITION
BY, and has no sequence, so those queries are not answered. PLUGINS is
empty, so extensions is not answered. The rest are absent, as they are on
MySQL.

### What the fixture builds

`models/singlestore/fixture` builds the core tables in SingleStore's own
syntax, with no foreign key. book is a reference table, because a unique key
of a sharded table must hold the shard key, and book_title_unique is on title
alone. It adds a function, a procedure, an aggregate and the four functions
behind it, a role granted to a group and the group to a user, a privilege,
rows with statistics, and a correlation between two columns of author.
`CORRELATE COLUMN` refuses to run with no database selected, even on a
qualified table, so the fixture selects one with USE, and the test runs the
setup on one connection and discards it afterwards.

### Which answers depend on who is asking

Parity asks as root, as a user with every privilege on the fixture schema,
and as one that can only read it, as for MySQL. Both lesser users see fewer
roles, role grants and privileges.

### What a second opinion found

Gemini and Gemini Pro were asked about each gap, as hard rule 14 requires, on
2026-09-30. Every lead was a stretch or a view that exists for MySQL's
clients and is empty: LINKS as foreign servers, EXTERNAL_TABLES as foreign
tables, PIPELINES as subscriptions, RESOURCE_POOLS as role settings,
DISTRIBUTED_PARTITIONS as partitioned tables, and an AUTO_INCREMENT column as
a sequence. TRIGGERS and TABLESPACES are empty. Both named the histograms for
extended statistics, and the histograms are over one column each. The view
of correlations covers two, and D149 shipped it.

### The fields that D205 fills

SingleStore fills `Table.AccessMethod` from `STORAGE_TYPE` (COLUMNSTORE or
INMEMORY_ROWSTORE), `Size`, `Rows`, `Persistence` (`temporary` for the
TEMPORARY TABLE of the session) and `Index.Using`, which holds the type that
`STATISTICS` records, such as BTREE, FULLTEXT, SHARD or COLUMNSTORE HASH.
`Options` is NULL, because `CREATE_OPTIONS` is always NULL. `Constraint.Enforced`
is NULL, because SingleStore claims MySQL 5.7. It has no `Partitions`, because
`PARTITIONS` lists nothing and a table is not split by PARTITION BY here.

## Snowflake and Amazon Redshift

`models/snowflake` answers 15 of the 65 and `models/redshift` answers 18.
Ken chose on 2026-09-30 to write both from the vendors' documentation before
an account or a cluster was provisioned (D144). Both ran on 2026-10-08.
D182 holds what Redshift found and D190 holds what Snowflake found. The tests
skip until dbrun resolves a connection string (D117).

Snowflake reads the INFORMATION_SCHEMA of the database the connection is in.
It has no KEY_COLUMN_USAGE, so the columns of a key come from SHOW PRIMARY
KEYS, SHOW UNIQUE KEYS and SHOW IMPORTED KEYS, which the pipe operator turns
into one statement (D203). It has no index outside a hybrid table. The fold is
upper case, as Snowflake documents.

### Measured on Snowflake

Measured on 2026-10-08 on a trial account, release 10.36.101, as the role
DBMETA_ROLE, which owns the database DBMETA and uses the warehouse DBMETA_WH.

- The server answers 13 of the 65 questions: schemas, databases, tables,
  columns, views, constraints, sequences, functions, comments, privileges,
  role grants, the current schema and the current user.
- One statement failed. INCREMENT is a keyword, so `s.increment` in the
  sequences statement is a syntax error. The statement now writes
  `s."INCREMENT"`. Every other statement, the fixture and the version query
  ran as written.
- The privileges statement named every object a table. It now joins
  information_schema.tables and reports the type of the object, so a grant on
  a view reads view.
- SNOWFLAKE.ACCOUNT_USAGE is refused to this role with "does not exist or not
  authorized". The model reads none of it, which is what the model assumed.
- A role that owns its database sees every database it can use in
  information_schema.databases, including SNOWFLAKE_SAMPLE_DATA. The
  databases query lists them all, because a database has no system flag.
- A constraint is reported with IS_DEFERRABLE of NO and INITIALLY_DEFERRED of
  YES, and ENFORCED of NO. The constraints query passes both through, so
  deferrable is false and deferred is true for every key. A consumer must not
  read deferred as meaning that the key is checked later.
- A primary key without a name reads as SYS_CONSTRAINT_ and a UUID.
- APPLICABLE_ROLES lists the grant of PUBLIC to the user twice. The role
  grants query passes the duplicate through.
- AUTOINCREMENT reports IS_IDENTITY of YES, and the column has no default
  value. The identity kind is by default.
- The view definition is the CREATE VIEW statement as it was written, with its
  own line breaks and tabs.
- A procedure shows its argument signature as `(A NUMBER, B NUMBER)` and a
  result type of `NUMBER(38,0)`, where a function with a VARCHAR result shows
  `VARCHAR(134217728)`.
- Ken granted CREATE USER and CREATE ROLE to DBMETA_ROLE, and D193 measured
  parity with three principals. INFORMATION_SCHEMA shows a role only what the
  role holds a grant on. A grantee with one table reads fewer rows of tables,
  columns, constraints, views, functions, sequences and privileges. A role
  with USAGE on the database and nothing in it also reads fewer schemas. A role
  with no grant has no current database and every catalog statement is
  refused. A made user runs on DBMETA_WH since 2026-10-09, because DBMETA_ROLE
  owns it and grants USAGE on it to each role the test makes (D203).
- The conformance target records that an AUTOINCREMENT column has no default,
  and that a view column reads as not nullable where its table column is.
  Snowflake is in `agreementExcluded`. Since D203 the keys agree with
  PostgreSQL, and the golden file has the primary keys and the constraint lines.
- `ChangePassword` ran against a user that the test made. All seven hostile
  passwords set and logged in, with a 13 character prefix because the account
  refuses a short password. A user cannot change its own password with the
  statement, because the server asks for MODIFY on the user.
- Every constraint reads ENFORCED of NO and RELY of NO beside the deferred
  flags above. APPLICABLE_ROLES still lists PUBLIC twice for the user, and
  ENABLED_ROLES lists it once.
- Not read and not answered: ACCOUNT_USAGE, and the kinds listed under
  Measured on 2026-10-09 below.
- The warehouse is XSMALL with a 60 second auto suspend.

### Measured on Snowflake on 2026-10-09

D203 measured what INFORMATION_SCHEMA and SHOW expose, on the same trial
account, release 10.36.101. The model answers 15 of the 65 questions: the 13
above, the columns of a constraint, and the session parameters as settings.

- The columns of a key. INFORMATION_SCHEMA has no view of them. SHOW PRIMARY
  KEYS, SHOW UNIQUE KEYS and SHOW IMPORTED KEYS list them, and the pipe
  operator, `SHOW X ->> SELECT ... FROM $1`, makes one statement of a SHOW and
  a select. The select can join INFORMATION_SCHEMA.COLUMNS to `$1`, so
  `primary_key` is a real fact and stays a plain bool. Three SHOW stages in one
  pipe read `$1` as the latest result, `$2` as the one before and `$3` as the
  first. SHOW IMPORTED KEYS names the referenced table and column, so
  constraint columns carry the foreign key target.
- A SHOW with no scope reads the current schema, and a session can change that.
  Every stage names a scope. SHOW listed 10803 primary key rows at once, so
  there is no cap of 10000.
- A bind parameter is refused after the pipe, with "invalid identifier '1'".
  Ken decided to write the values into the statement, so the Snowflake dialect
  sets `Info.Literal` (D78) and every statement of the model has its values
  written in. The scope of each SHOW is a derived value (`Binding.Derived`):
  `IN TABLE "schema"."table"` for an exact schema and table, `IN SCHEMA
  "catalog"."schema"` for an exact catalog and schema, and `IN DATABASE`
  otherwise. SHOW refuses a schema without its database, and a statement cannot
  name the current database, so the key statements take a `catalog` parameter.
  A pattern is exact when it has no `%`, `_` or backslash, so a name with an
  underscore reads the database scope. A missing table in the table scope is a
  server error. A session with no current database makes SHOW IN DATABASE read
  every database the role can see, so the constraint columns statement keeps
  the rows of CURRENT_DATABASE().
- GET_DDL answers one text for a schema, with `primary key (A, B)` inside. It
  is a function, it is prose, and a schema of thousands of tables does not fit
  in one value, so it is not used. Gemini and DeepSeek both said that one
  plain select cannot read the columns of a key. The pipe operator was the lead
  that neither of them named.
- ENFORCED is in INFORMATION_SCHEMA.TABLE_CONSTRAINTS, and `Constraint.Enforced`
  carries it. It reads false for every key.
- `INFORMATION_SCHEMA.FUNCTIONS` has VOLATILITY, which the model had marked as
  not recorded. It reads `volatile` for a function that has no other keyword,
  and a procedure has none.
- A dynamic table is a base table to TABLE_TYPE. IS_DYNAMIC, IS_ICEBERG and
  IS_HYBRID say which, so the Tables type now reads dynamic table, iceberg
  table or hybrid table. A hybrid and an Iceberg table were not created, so
  those two words are read from the flags and not measured.
- Settings read SHOW PARAMETERS, with the key as the name and the level as the
  context. The session read 161 parameters. The statement has no access and no
  display.

What each unread object maps onto, measured on the trial account:

| Object | Source | Answer |
| --- | --- | --- |
| Tags | SHOW TAGS, and the TAG_REFERENCES table functions, one object per call | no kind: unsupported, because only Snowflake has a tag |
| Masking policies | SHOW MASKING POLICIES | the trial refuses CREATE as "Unsupported feature". The SHOW answers no rows. No kind: unsupported |
| Row access policies | SHOW ROW ACCESS POLICIES | the trial refuses the feature. It maps onto Policies in principle, and is unsupported until an account can make one |
| Stages, file formats, pipes | INFORMATION_SCHEMA.STAGES, FILE_FORMATS, PIPES | answered by one statement each, and no kind: unsupported |
| Streams, tasks | SHOW STREAMS, SHOW TASKS | answered, and no kind. A task runs on a schedule and a stream records changes, so neither is a trigger: unsupported |
| External tables | INFORMATION_SCHEMA.EXTERNAL_TABLES | the view answers no rows, because an external table needs an external stage and a trial has none. Tables lists the type from TABLE_TYPE |
| Dynamic tables | INFORMATION_SCHEMA.TABLES, IS_DYNAMIC | a Tables row of type dynamic table, measured |
| Materialized views | INFORMATION_SCHEMA.TABLES | the trial refuses the feature. Tables would read materialized view from TABLE_TYPE |
| Hybrid tables and indexes | INFORMATION_SCHEMA.INDEXES and INDEX_COLUMNS | the trial refuses a hybrid table. Both views exist and answer no rows, so Indexes and IndexColumns are answerable in principle and are not added, because no row was read |
| Sequences, functions, procedures | INFORMATION_SCHEMA | answered, and read again on 2026-10-09 |
| Roles | SHOW ROLES, which answers with the pipe operator | answerable, and not added: Role has a bool for superuser, create role, create database and replication, which Snowflake has no source for |
| Default privileges | SHOW FUTURE GRANTS IN DATABASE | answers no rows here. Not added |
| Routine parameters | the argument signature | one text, so unsupported. SHOW PARAMETERS IN FUNCTION lists session parameters and not arguments |

Rule 14 ran with Gemini and DeepSeek on 2026-10-09. Gemini answered one short
question. DeepSeek Flash ran out of tokens twice on the long one and DeepSeek
Pro answered the short one. Each lead was run on the server. SHOW ROLES, SHOW
INDEXES, SHOW FUTURE GRANTS, SHOW EXTERNAL TABLES and SHOW PARAMETERS all ran.
The settings lead was added. The models called the policies and the external
tables present under other names, which matches the table above.

Cost, on a scratch schema of 2000 tables with 14000 columns, a primary key of
four columns, a unique key and a foreign key of four columns on each, in the
database DBMETA (D47). Each number is one run on an XSMALL warehouse and varies
by a few seconds. The names have no underscore, so each reads its scope.

| Arguments | Columns | Constraint columns |
| --- | --- | --- |
| one table, schema and table exact | 7 rows, 1.4 s | 9 rows, 1.4 s |
| one schema, database and schema exact | 14000 rows, 19.3 s | 17996 rows, 18.4 s |
| a wildcard table | 777 rows, 6.4 s | 999 rows, 32.2 s |
| the whole database | 14000 rows, 8.0 s | 17996 rows, 21.0 s |

Before the scope, with the filter in Go, one table cost 10.1 s for columns and
24.4 s for constraint columns. A plain read of the columns of one table cost
1.5 s. The schema was the only one in the database, so its scope read the same
rows as the database. A wildcard or a name with an underscore reads the
database, and the cost grows with it, because SHOW has no filter. The
constraints statement does not use SHOW and read one table in 0.8 s and the
schema in 24.6 s. D203 holds both sets of numbers.

The 2000 tables took 8 minutes to create, and every scratch schema was dropped.

Redshift reads the pg_catalog tables that PostgreSQL 8.0 already had, from
which Redshift was built, because the postgres model's statements need 9.6.
D204 added the kinds that need an SVV view, and the notes below say where each
one reads one.

### Measured on Redshift Serverless

Measured on 2026-10-08 on Redshift Serverless in us-east-1, release
1.0.434008, whose banner says PostgreSQL 8.0.2. Every statement ran,
and roles needed a cast. The fixture built on the first try.

- The server answers 18 of the 65 questions: schemas, databases, tables,
  columns, views, constraints, constraint columns, functions, roles, role
  grants, privileges, column privileges, default ACLs, languages, settings,
  comments, the current schema and the current user. The other 47 are covered
  by the last note of this list.
- pg_catalog hides nothing from a user without a grant. An owner, a grantee
  and a user with no grant on the fixture all read the same rows as the
  administrator. The SVV views are the ones that filter by user. A user reads
  only its own rows of SVV_USER_GRANTS, SVV_ROLE_GRANTS, SVV_USER_INFO and
  SVV_ALL_TABLES, and only the grants that name it in SVV_COLUMN_PRIVILEGES
  and SVV_RELATION_PRIVILEGES. SVV_ROLES and pg_group show every user the
  same rows. Parity (D204) records that role grants and column privileges
  read fewer rows for a user that is not the administrator, and that
  privileges, roles, default ACLs, languages, settings and constraint columns
  read the same rows for everybody.
- pg_catalog also lists the objects of Redshift itself. The schemas
  pg_auto_copy, pg_mv and pg_s3 are hidden by default beside the ones that
  were already named. A schema of the user is the only one left.
- pg_user lists a user of IAM identity, such as IAM:RootIdentity, beside the
  database users. The roles query reports it as a user that can log in.
- valid_until is the type abstime in pg_user, which Redshift refuses to cast
  to text. The statement casts it to a timestamp first. A user with no expiry
  reads as absent, and the administrator reads as infinity.
- An IDENTITY(seed, step) column and a GENERATED ALWAYS AS IDENTITY column
  have the default `"identity"(<oid>, 0, '1,1'::text)`, and an INSERT of a value
  into one fails with "cannot set an identity column to a value". The column
  reports the identity kind a, which means always. A GENERATED BY DEFAULT AS
  IDENTITY column has the default `default_identity(<oid>, 0, '5,2'::text)`,
  accepts a value and reports the kind d. D182 read the first from the
  documentation and D204 measured both.
- The collation of a column is in SVV_COLUMNS, as case_sensitive or
  case_insensitive, and it is absent for a column that is not a character
  type. A correlated subquery on SVV_COLUMNS fails with "plan should not
  reference subplan's variable", so the columns statement joins it. The join
  costs about one second more for 8,500 columns and nothing for a schema with
  no rows. Redshift has no generated column, and GENERATED ALWAYS AS
  (expression) is a syntax error. It has no CHECK constraint, and a table
  definition with one is refused.
- A grant to a user or a group is in the ACL of pg_class, pg_namespace and
  pg_proc, which every user reads in full. A grant to a role is not in the
  ACL. It is only in SVV_RELATION_PRIVILEGES, which shows a user the rows that
  name it, so the privileges kind does not report a grant to a role. A column
  grant is only in SVV_COLUMN_PRIVILEGES. pg_attribute has no ACL.
- Redshift has three kinds of principal that hold a grant: a user, a group in
  pg_group and a role in SVV_ROLES. The roles kind lists all three. A group
  and a role cannot log in. SVV_ROLES lists five roles of the system, such as
  sys:dba, that start with sys:.
- Redshift has no unnest, so the constraint columns statement stands a series
  of 32 positions in for it, and joins the key array at each position.
- Every password that Redshift documents as forbidden sets and logs in
  (D204). The server refuses a password of fewer than 8 characters or one with
  no digit, no upper case letter or no lower case letter, and it says so in
  words.
- A Redshift string has no E'' form, so a newline is chr(10).
- A foreign key reads with the schema qualified and the name of a table
  quoted when it is a keyword, as in `REFERENCES dbmeta_fixture."region"(...)`.
- A view definition ends with a semicolon.
- A function has no pg_get_functiondef, so its definition is absent and the
  source is the body between the dollar quotes.
- Spectrum is measured (D221). The default IAM role of the namespace reads the
  Glue database `dbmeta_spectrum` and cannot write to Glue, so the fixture makes only
  the external schema `dbmeta_spectrum` over the Glue table `spectrum_t` that
  dbsetup made, and it skips the step when the namespace has no default role. The
  external schema is a row of pg_namespace, so Schemas and Privileges list it.
  The table is not in pg_class or pg_table_def, and Tables and Columns read it
  from SVV_EXTERNAL_TABLES and SVV_EXTERNAL_COLUMNS as an `external table`. Its
  size and rows are absent, its options are the location and the serde library,
  and its columns carry the Glue type and are nullable. SVV_EXTERNAL_PARTITIONS
  has no row for a table with no partition, and no partition is read. A user
  needs USAGE on the external schema to see the table, and the grantee has it.
- A UNION in Tables or Columns is refused when it reads pg_user itself, so a
  derived table names the two columns that are read. The CASE of the identity
  column is cast to varchar(1) and the primary key is a join, because a UNION
  gives a CASE the width of its ELSE and does not take a correlated subquery.
- Rule 14 of AGENTS.md. Gemini and DeepSeek sorted the unanswered kinds. Both
  named SVV_ROLE_GRANTS, SVV_RELATION_PRIVILEGES, SVV_COLUMN_PRIVILEGES,
  SVV_EXTERNAL_TABLES and SVV_DATASHARES. DeepSeek also named SVV_COLLATIONS,
  SVV_SEQUENCES, SVV_POLICIES and SVV_CONSTRAINT_COLUMNS, and Gemini named
  SVV_RLS_POLICY. The first four do not exist. SVV_RLS_POLICY does and is not
  read. Both agreed that indexes, tablespaces, large objects, domains,
  extensions, triggers, sequences, partitions and inheritance are absent. The
  server has a pg_tablespace that lists only pg_default and pg_global, and a
  pg_trigger and a pg_inherits that were not read, because Redshift has no
  trigger and no inheritance. It has no pg_collation, pg_policy, pg_extension
  or pg_enum.
- Left unanswered with a source in pg_catalog: the routine parameters, types,
  casts, aggregates, operators, conversions and column statistics. The row
  level security policies are in SVV_RLS_POLICY and the datashares in
  SVV_DATASHARES, which are empty here. There is no collation kind, because
  Redshift has two fixed collations and no list of them. Redshift has no
  index, sequence or trigger.
- The database list includes awsdatacatalog, padb_harvest and sys:internal,
  which Redshift keeps for itself. They are not hidden, because a database
  has no system flag.

### The describe fields (D207)

D198 and D199 gave Tables, Columns, Functions and Schemas fields that only
PostgreSQL filled. D207 measured each one on Snowflake 10.36.101 and Redshift
1.0.434008.

| Field | Snowflake | Redshift |
| --- | --- | --- |
| `Table.Owner` | `TABLE_OWNER` | `usename` of a join to `pg_user`, because `pg_get_userbyid` is refused beside SVV_TABLE_INFO (D212) |
| `Table.Persistence` | `temporary` from `IS_TEMPORARY`, `transient` from `IS_TRANSIENT` and `permanent`. NULL for a view | `temporary` for a table in a `pg_temp` schema, and `permanent`. Redshift has no unlogged table |
| `Table.Size` | `BYTES`, the active data. Time travel and fail safe data are not in it, so it is a bound | `size` of SVV_TABLE_INFO times 1048576, a bound because the view counts blocks of 1 MB. NULL unless `WithTableInfo` is set, and NULL for a view and for an empty table (D212, D232) |
| `Table.Rows` | `ROW_COUNT`, kept and not estimated | `tbl_rows` of SVV_TABLE_INFO, which counts rows marked for deletion until a vacuum. NULL unless `WithTableInfo` is set, and NULL for a view and for an empty table (D212, D232) |
| `Table.Options` | `retention_time` in days, and `cluster_by` when a clustering key is set | `diststyle` and `sortkey1` of SVV_TABLE_INFO, as `diststyle=KEY(event_id), sortkey=happened`. NULL unless `WithTableInfo` is set, and NULL for a view and for an empty table. An external table always has its location and serde (D212, D232) |
| `Column.Compression` | NULL. Snowflake compresses by itself and records no choice | the encoding from `pg_attribute.attencodingtype`, named by the numbers that `pg_table_def` shows: 0 raw, 1 bytedict, 2 delta, 3 lzo, 4 runlength, 5 delta32k, 7 text255, 15 mostly8, 16 mostly16, 17 mostly32, 18 text32k, 19 zstd and 20 az64. A number that was not measured is returned as the number |
| `Column.Storage`, `StatsTarget` | NULL | NULL. `attstorage` is there with the value PostgreSQL 8.0 gives, and Redshift has no TOAST, so it says nothing. `attstattarget` is -1 for every column |
| `Function.Prosrc` | `function_definition` and `procedure_definition`, the same text as `source` | `prosrc`, the same text as `source` |
| `Schema.Access` | NULL. A grant on a schema is not in INFORMATION_SCHEMA | `nspacl`, one grant on each line |
| `Constraint.Enforced` | `ENFORCED`, from D203 | NULL. Redshift declares a key and does not enforce it, but no catalog column says so |
| `Column.Collation` | `COLLATION_NAME`, from D190 | `case_sensitive` or `case_insensitive`, from D182 |
| Index fields | Snowflake has no index outside a hybrid table, and that is not read | Redshift has no index |
| `Partitions`, `Policies`, `NotNulls`, `Inherits` | none of the four | none of the four |

D207 first left SVV_TABLE_INFO out, for three defects and one refusal. D212
reads it, after Ken decided that the administrator grants SELECT on it. The
grant works on Redshift Serverless 1.0.477953. The statement reads the view as
a derived table joined by table id. It replaces `pg_get_userbyid` and
`obj_description` with joins, because Redshift refuses the two beside the view.
It trims the padded names, so that `LIKE 's5'` with no wildcard matches. It
compares the boolean parameter as `@with_system = TRUE`, and it has no cast of
`relkind`, because Redshift refuses each of those beside the view too. The view
lists a table only when it holds a row, so an empty table has NULL for the
three fields.

D232 changed the cost of that decision. A user without SELECT on
SVV_TABLE_INFO got the error "permission denied for relation svv_table_info"
(SQLSTATE 42501) from the whole `tables` kind, and so `\dt`, `\dt+`, `\d t` and
`\dv` of usql failed for every user who is not a superuser. Ken chose that the
size and the rows are opt in. The default statement never names the view, so
`Size`, `Rows` and `Options` are NULL for a table and a view, and every user
reads the kind. A caller that wants them sets `Args.WithTableInfo`, the
parameter `with_table_info`, and the statement then joins the view as D212
describes. The error stays 42501 for a user with no grant, and
`GRANT SELECT ON svv_table_info` to a user, group or role lifts it. The read
costs about 0.5 s more than before, whatever the number of rows returned: 0.27
s without the view and 0.77 s with it, for six tables and for one.

An ordinary user reads every other Redshift query with no grant. The parity
test makes an owner, a grantee and a stranger, none of them a superuser and
none holding a grant on the view, and no query is refused for any of them. The
three differ from the administrator only where an SVV view shows a user its own
rows, which D204 recorded. The external tables of a Spectrum schema, which
read SVV_EXTERNAL_TABLES and SVV_EXTERNAL_SCHEMAS, need only USAGE on the
schema, when the namespace has a default IAM role to make the schema (D221).

The NULL hazards of D197: Snowflake has no INCLUDE column, no default
privilege and no role setting that the model reads. Redshift has default
privileges, and `DefaultACL.Schema` is NULL for one made with no IN SCHEMA. The
Redshift fixture builds one, and the scan reads it.

## Apache Impala

`models/impala` answers 11 of the 65 on 4.4.1 and 4.5.2, measured on
2026-09-30 in the one container dbrun builds (D145), with sclgo/impala-go,
which is what usql uses.

### A walk of SHOW statements

Impala has no catalog a SELECT can read. Schemas, databases, tables, views,
columns, column statistics, functions, aggregates and settings are each a
walk (D146): SHOW DATABASES, and then SHOW TABLES IN and SHOW VIEWS IN each
database, DESCRIBE FORMATTED each table for its type and comment, DESCRIBE
and SHOW COLUMN STATS each table for its columns, SHOW CREATE VIEW each view,
and SHOW FUNCTIONS IN and SHOW AGGREGATE FUNCTIONS IN each database. The metastore makes a new table
external, so a table the fixture makes reads as an external table. A walk costs one statement for
each database it reaches, and one for each table where it goes deeper. The
caller's patterns are matched in Go, because a SHOW statement takes none.
The current schema and the current user are one SELECT each.

### What it does not answer

A primary key and a foreign key are information Impala keeps and no SHOW
statement lists, so constraints and constraint columns are not answered.
Impala has no index, trigger or sequence, and authorization is off in the
image, so roles and privileges are not answered. A function of a user's own
needs a library file, and SHOW FUNCTIONS records no parameter names, so
routine parameters are not answered.

### Which answers depend on who is asking

Nothing. The image configures no authentication, so every user is the same
principal, and Impala is exempt from parity with that reason.

### The describe fields (D209)

D198 to D203 gave the root types fields that only PostgreSQL filled. The Impala
walk already runs `DESCRIBE FORMATTED` for each table, so the fields it holds
cost no statement. D209 fills them on 4.4.1 and 4.5.2.

| Field | Apache Impala |
| --- | --- |
| `Table.Owner` | the Owner row of `DESCRIBE FORMATTED` |
| `Table.Persistence` | permanent for a table and NULL for a view |
| `Table.AccessMethod` | the InputFormat row. NULL for a view |
| `Table.Size`, `Rows` | the `totalSize` and `numRows` table parameters, which `COMPUTE STATS` fills. NULL where Impala writes -1 |
| `Table.Options` | NULL. The parameters mix statistics, times and settings |
| Every other new field and kind | NULL or not answered, as in Hive. The walk reads no constraint, index or partition |

## rqlite

`models/rqlite` answers 14 of the 65 on 9.4.5 and 10.3.6. It was first
measured on 2026-09-30 through the driver of `github.com/rqlite/gorqlite`
(D148), and again on 2026-10-01 through dbimp's driver, which usql uses, from
dbimp v0.8.0 (D151). rqlite runs SQLite 3.53 behind an HTTP API, so every statement is the
sqlite3 model's, shared with `Query.Share`. It answers what SQLite answers,
and the SQLite section above says why the rest is not answered. Every check
the SQLite tests make runs on rqlite too, and the conformance report is line
for line SQLite's.

### What rqlite adds, and why none of it is an answer

rqlite reads its users and their permissions from the file that `-auth`
names, and no SQL statement reaches that file. Its SQLite has the user
authentication extension compiled in: `auth_enabled()`, `auth_user_add()`,
`auth_user_change()`, `auth_user_delete()` and `authenticate()` are in
`pragma_function_list`. `auth_enabled()` returns 0 and there is no
`sqlite_user` table, so the extension holds no user. `Roles`, `Privileges`
and `CurrentUser` stay unanswered.

The version is the release of SQLite that the server runs. No SQL statement
names the rqlite release, and only the HTTP API reports it.

Gemini and DeepSeek were asked about the 42 unanswered kinds on 2026-09-30,
as hard rule 14 requires. Both said rqlite adds nothing that SQL can read.
Their SQLite leads were `sqlite_sequence`, `sqlite_stat1`, `sqlite_stat4`,
`pragma_module_list`, and the type `a` of `pragma_function_list`. The SQLite
section rejects each of them but `sqlite_stat4`, and rqlite's SQLite is built
without `SQLITE_ENABLE_STAT4`, so it has no `sqlite_stat4` to read. Its
compile options name `ENABLE_DBSTAT_VTAB`, and `dbstat` reports the pages of
each table and index, which is a size and not one of the 65.

### Parity

The entry declares `dbmeta_user`, who can query and execute and nothing else.
rqlite has no grant on a table, so the user reads every answer the
administrator reads, and the section in `test/testdata/parity.txt` is empty.

### The describe fields (D208)

Every statement is the sqlite3 model's, so rqlite fills the same fields as
SQLite: the persistence of a table, and the predicate, the clustered flag, the
definition, the method and the constraint type of an index. The tests run the
same checks on it.

## libSQL

`models/libsql` answers 14 of the 65 on 0.24.33, measured on 2026-10-01
through dbimp's libSQL driver, which dburl names and usql imports, from dbimp
v0.10.0. libSQL is the fork of SQLite by Turso, and sqld is the server that
serves it over HTTP. sqld 0.24.33 runs SQLite 3.45.1. Every statement is the
sqlite3 model's, shared with `Query.Share`, and the model reads
`sqlite_schema` and the table valued pragmas, as the SQLite section above
says. Every check the SQLite tests make runs on libSQL too, and the
conformance report is line for line SQLite's. See D160.

### The vector index, which is what libSQL adds

libSQL adds a vector index. `CREATE INDEX i ON t (libsql_vector_idx(c))`
makes one on a column of a vector type, such as `F32_BLOB(3)`.
`pragma_index_list` reports it as an ordinary index that someone created, and
`pragma_index_xinfo` reports its one column as an expression. So the shared
statement called it `btree`, which it is not.

libSQL also keeps two kinds of table for vector indexes, and the shared
statements listed both as tables a person made:

- `libsql_vector_meta_shadow` holds a row for each vector index. libSQL makes
  it with the first vector index and never drops it.
- Each vector index keeps its data in a table named for the index with
  `_shadow` after it, and that table has an index named with `_shadow_idx`
  after the index name. Dropping the vector index drops both.

`pragma_table_list` reports both kinds as `table` rather than `shadow`. A
user can drop the shadow table, which breaks the vector index, and the
`libsql_` prefix is not reserved: a user can make `libsql_x`. So the prefix
is not a test, and the model names the one table.

Two pieces of the sqlite3 model's statements now have a fragment for libSQL,
which gates on the version key `libsql`. The libsql model records that key,
with an unknown version, because no SQL statement names the sqld release.
The system filter of every statement that reads `sqlite_schema` leaves out
`libsql_vector_meta_shadow` and each shadow table, unless the caller asks for
the system objects. The type of an index is `diskann` for a vector index,
which is the name libSQL gives the method in its `type=diskann` option. A
vector index is found by the statement that made it, which names
`libsql_vector_idx`, and by the shadow table that libSQL made for it. The
subquery that finds them does not refer to the outer row, so SQLite runs it
once for each statement. `TestLibSQLVectorIndex` checks all of this.

A vector column reads its declared type, such as `F32_BLOB(3)`, which is
already the answer. Its options, such as `metric=cosine`, are only in the
text of the statement in `sqlite_schema.sql`, and dbmeta does not parse DDL.

### What libSQL adds that is not an answer

`ALTER TABLE ... ALTER COLUMN` changes a column, and the pragmas read the
result. It is a statement and not an object.

`RANDOM ROWID` makes a table whose rowid is random. No pragma reports it,
and `pragma_table_list` reports the table as an ordinary one. It is only in
the text of the statement in `sqlite_schema.sql`.

A function in WebAssembly, `CREATE FUNCTION ... LANGUAGE wasm`, is refused by
sqld 0.24.33 with a parse error, so this build has none to list.

`libsql_server_database_name()` returns the namespace of the request,
`default` on this server. The `Databases` statement reads
`pragma_database_list`, which names `main`, as on SQLite. A namespace is
what sqld calls a database, and the list of namespaces is in the admin HTTP
API, which no SQL statement reaches.

sqld registers about 200 functions of its own beside SQLite's, most of them
from the sqlean extensions: math, text, crypto, fuzzy matching, regular
expressions, UUIDs and statistics. `pragma_function_list` reports each one
with `builtin` 0, so `Functions` lists them without the system objects, and
its `language` is `extension`. Six of them have the name of a function built
into SQLite: `concat`, `concat_ws`, `ltrim`, `rtrim`, `octet_length` and
`soundex`. So those names have two rows, one for each language, and
`TestSQLiteFunctions` keys on the language too.

### What a second opinion found

Gemini and DeepSeek were asked about the 42 unanswered kinds on 2026-10-01,
as hard rule 14 requires. Both said libSQL adds nothing that SQL can read for
any of them. Their leads were these, and each was run on 0.24.33:

- `sqlite_sequence` for `Sequences`, from both. The SQLite section rejects
  it, and libSQL changes nothing.
- `sqlite_stat1` for `ColumnStats` and `sqlite_stat4` for `ExtendedStats`,
  from Gemini. This build has `ENABLE_STAT4`, where rqlite's has not. But
  sqld refuses `ANALYZE` and `PRAGMA optimize` with "unsupported statement",
  so neither table can exist. The SQLite section rejects both for SQLite
  anyway.
- The type `a` of `pragma_function_list` for `Aggregates`, from both. On
  libSQL it matches 26 functions, which are the sqlean statistics functions
  such as `median` and `stddev`. `sum` and `count` still report `w`, so the
  reason the SQLite section gives still holds. DeepSeek wrote the type as
  `aggregate`, which matches nothing.
- The declared types of the columns for `Types`, from DeepSeek. The SQLite
  section rejects it.

`pragma_module_list` names `vector_top_k`, the table valued function that
searches a vector index. It is a module, which the SQLite section rejects
for `Extensions`.

### What the fixture builds

The fixture is SQLite's and one table more: `embedding`, with a vector
column and a vector index, `embedding_vector`. The core objects are SQLite's,
so the conformance report is SQLite's.

### Parity

The entry declares `dbmeta_user`, whose token has the claim `{"a":"ro"}` and
can read and not write (D153). sqld has no grant on a table, so the user
reads every answer the administrator reads, and the section in
`test/testdata/parity.txt` is empty.

### The describe fields (D208)

Every statement is the sqlite3 model's, so libSQL fills the same fields as
SQLite. The method of an index is `diskann` for a vector index and `btree` for
the rest, and the tests run the same checks on it.

## InfluxDB 3

`models/influxdb` answers 9 of the 65 on InfluxDB 3 Core, measured on
2026-10-01 on 3.11.5 and on 2026-10-07 on 3.10.6, 3.11.6 and 3.12.0, through dbimp's influxdb driver, which is what dburl's
influxdb scheme opens and what usql uses. InfluxDB 3 answers SQL with Apache
DataFusion, and DataFusion keeps an information_schema, which the model
reads. InfluxQL is the dialect influxql, which answers InfluxDB 1, 2 and 3,
and `models/influxql` reads it, under InfluxQL below. See D152 and D165.

### What it answers

Schemas, the current schema, tables, columns, triggers, functions, aggregates,
routine parameters and settings.

A trigger is a processing engine trigger. It runs a Python plugin on a write
to one table, on a write to any table, on a schedule or on a request, and
`system.processing_engine_triggers` lists it for the database of the
connection, with the specification as JSON text. The table is read out of
`{"single_table_wal_write":{"table_name":"author"}}`, and it is empty for the
other three. The definition is the specification as the server keeps it. The
plugin file, its arguments and its error behavior are not carried, because
`Trigger` has no field for them. The setup of the entry makes one trigger, on
`author`, with a plugin that does nothing (D170).

A measurement is a table in the schema iox. Its tags and fields are its
columns, and so is time, which is the only column that is NOT NULL. A tag
reads `Dictionary(Int32, Utf8)`, which is how DataFusion keeps it, and a
field reads its Arrow type. DataFusion counts a column's position from 0 and
the model adds 1. The columns of a measurement come back in the order of
their names, which is how InfluxDB 3 keeps them, and not in the order a point
wrote them.

There is no `current_schema()`. `df_settings` records the default catalog and
the default schema, public and iox, and the current schema reads them.

The functions are DataFusion's own, 300 names, and InfluxDB 3 has no CREATE
FUNCTION, so they are listed only with the system objects. `routines` has one
row for each name and return type, and `parameters` has one set of rows for
each overload, numbered by `rid`, with the return type as its OUT row. So a
function is one row for each overload, with the id `name(rid)`, such as
`date_bin(1)`, which routine parameters carries too. A function with no
parameter rows is one row for each return type, with no id. first_value,
last_value and nth_value are both aggregates and window functions, and read
agg.

The version is the release of DataFusion, which `version()` returns, such as
51.0.0. No SQL statement names the InfluxDB release, which only `GET /ping`
reports, and usql reads it from there through the driver's raw connection.

### What it cannot answer, and why

47 kinds. InfluxDB 3's SQL writes nothing, so it has no CREATE of any kind,
and most kinds are absent from the product: indexes, constraints, sequences,
types, domains, collations, comments, roles and privileges among them.

`Databases` is not answered. A query names its database in the request, and
no SQL statement lists the others. Only `GET /api/v3/configure/database`
does.

`Views` is not answered. `information_schema.views` lists every table, the
measurements included, with no definition, and InfluxDB 3 has no CREATE
VIEW, so the only views are the server's own, `processing_engine_logs` and
the views of information_schema.

### What a second opinion found

Gemini and DeepSeek were asked about the 48 kinds on 2026-10-01, as hard rule
14 requires. Both named `system.processing_engine_triggers` as triggers, which
held, and the model answers it. Gemini named `schemata` for databases, `views` for views, the
types of `columns` for types, the distinct and last value caches for indexes,
and `parquet_files` for partitioned tables and column statistics. DeepSeek
rejected the caches and `parquet_files`. `schemata` holds iox and system and
not the databases. `views` lists the measurements. The Arrow type of a column
is not a type catalog. A cache is not an index, and a Parquet file with a
time range and a row count is neither a declared partition nor a column
statistic. Each is a stretch, and D43 says to leave a stretch unanswered.

### What the fixture builds

`models/influxdb/fixture` writes four measurements, author, book, region and
shipment, with the INSERT that dbimp's driver turns into line protocol, and a
tag, an integer, a float, a string and a boolean among their columns.
InfluxDB 3's SQL has no DROP, so the fixture has no teardown, and writing it
again replaces its points.

### Which answers depend on who is asking

Nothing. InfluxDB 3 Core has one kind of token, the administrator's, so there
is no lesser principal, and InfluxDB is exempt from parity with that reason.

## Neo4j

`models/neo4j` answers 17 of the 65 on 2026.09.0 and 13 on 5.26.31, measured
on 2026-10-01 through dbimp's neo4j driver, which is what dburl's neo4j scheme
opens and what usql uses. Both releases are Tested. D162 holds the mapping
and the reasons for it, for Ken to review.

### It reads SHOW commands and procedures

Neo4j has no relational catalog. Its catalog is a set of SHOW commands, such
as SHOW DATABASES, SHOW INDEXES and SHOW CONSTRAINTS, and a set of
procedures, such as `db.labels()`. A SHOW command takes YIELD, WHERE and
RETURN, so it filters and projects like a SELECT, and every kind is one
statement.

A database is the schema. A database has no namespace inside it, and each
statement reads the database the connection is in, which is the path of the
URL. So Schemas and CurrentSchema return that database alone, and every
object names it as its schema. Databases lists every database on the server.

A node label and a relationship type are the two kinds of table, with the
types `node label` and `relationship type`. An index and a constraint are on
one of them.

Four things about writing Cypher for it, all measured:

1. Cypher has no LIKE. A pattern is turned into a Java regular expression
   inside the statement, one character at a time. `TestNeo4jPatterns` checks
   it against `dbmeta.Like`, with `.`, `(`, `[`, a trailing backslash and an
   escaped `_` among the patterns.
2. On 5.26 a SHOW command cannot be in a subquery or a UNION, and no UNWIND
   can follow it. It can hold a subquery expression, so `COLLECT { CALL
   db.info() ... }` names the database in each row on both releases.
3. From 2026.05, in Cypher 25, SHOW INDEXES, SHOW CONSTRAINTS, SHOW FUNCTIONS
   and SHOW PROCEDURES can be joined with other clauses. SHOW USERS, SHOW
   ROLES and SHOW PRIVILEGES cannot, on any release.
4. An administration command, such as SHOW USERS, runs from the database the
   connection is in, and Neo4j sends it to the system database itself.

### What it answers

Databases, schemas, the current schema, tables, indexes, constraints,
aggregates, roles, role grants, role settings, privileges, the current user
and settings, on both releases. Index columns, constraint columns, functions
and routine parameters from 2026.05, because each needs a SHOW command joined
with other clauses. 5.26 reports `TooOld` for those four.

Six answer with an analogue:

| Kind | Neo4j | Why it is a fair answer |
| --- | --- | --- |
| `Schemas` | the database of the connection | a database has no namespace inside it, and no statement reads the labels of another database |
| `Tables` | node labels and relationship types | an index and a constraint are on one of them, and `db.labels()` lists a label from the count Neo4j keeps, not from the data |
| `Roles` | users | a user logs in and holds roles. A role holds privileges and cannot log in, and SHOW ROLES cannot be joined with SHOW USERS |
| `RoleGrants` | the roles each user holds | SHOW ROLES WITH USERS, with PUBLIC for every user |
| `RoleSettings` | the home database of a user | the one setting Neo4j keeps for a user, which decides where a session that names no database runs. D162 asks Ken to confirm it |
| `Privileges` | SHOW PRIVILEGES, grouped by graph, segment and resource | a privilege on one property is a row of its own, with the property in the type, such as `property(rating)` |

A node key and a relationship key read `primary key`, a uniqueness constraint
`unique`, and an existence constraint `not null`. Any other constraint keeps
the Neo4j name in lower case. A full text index on two labels is one row, with
its table written `book|author`, and a pattern on its table matches either
label. A token lookup index has an empty table.

Functions and procedures belong to the server, so their schema is empty and
the namespace is part of the name, such as `db.labels`. A procedure is
always listed, because SHOW PROCEDURES does not say which are built in. The
columns a procedure yields read mode `table` in routine parameters.

### What it cannot answer, and why

39 kinds on 2026.09, and 43 on 5.26.

`Columns` is the largest gap. Neo4j keeps no catalog of the properties of a
label. The only source is `db.schema.nodeTypeProperties()` and
`db.schema.relTypeProperties()`, which read every node and every
relationship. On 2026.09.0 the first took 0.37 seconds for 1 million nodes and
0.95 seconds for 4 million, so its cost grows with the data, and D47 forbids
that. Ken chose on 2026-10-01 to leave Columns unsupported for that reason.
`TestNeo4jColumnsIsNotSupported` holds it. The same test was applied to every
other kind. `db.labels()` and `db.relationshipTypes()` took 0.015 seconds
together on an empty database and 0.024 seconds with 8 million nodes, on
5.26.31, so they read the counts and not the data.

Six more are close and rejected, which D162 lists with the reasons:
`AccessMethods` from the providers of existing indexes, `TextSearchConfigs`
from the full text analyzers, `Types` and `Domains` from the property types,
`Casts` from the conversion functions, `ForeignServers` and `UserMappings`
from a remote database alias, and `ColumnStats` and `EnumValues`, which read
the data.

The rest are absent from the product: views, sequences, triggers, comments,
collations, conversions, languages, large objects, event triggers, operators
and their classes and families, extensions, extended statistics,
publications, subscriptions, tablespaces, partitioned tables, foreign data
wrappers, foreign tables, default privileges and the other text search kinds.

### What a second opinion found

Gemini and DeepSeek were asked about the kinds it cannot answer on 2026-10-01, as hard rule
14 requires. Each lead was run against 2026.09.0.

| Lead | Who | What the server says |
| --- | --- | --- |
| `AccessMethods` from the providers in SHOW INDEXES | both | it lists the providers of indexes that exist, such as `range-1.0`, and nothing lists the providers no index uses. A stretch |
| `Casts` from conversion functions such as `toInteger` | both | they are in SHOW FUNCTIONS, which Functions reads. No catalog lists a cast from one type to another |
| `Types` from the Cypher property types | both | SHOW TYPES is a syntax error, and no procedure lists the types |
| `Domains` from property type constraints | Gemini | a property type constraint is a constraint, and Constraints reads it |
| `TextSearchConfigs` from SHOW FULLTEXT ANALYZERS | Gemini | there is no such command. `db.index.fulltext.listAvailableAnalyzers()` lists 45 analyzers with their stop words, and an analyzer has no parser or dictionary to report. A stretch |
| `ExtensionObjects` from SHOW PROCEDURES and SHOW FUNCTIONS | DeepSeek | no catalog says which plugin a routine came from, and `dbms.components()` lists only the kernel and Cypher |
| `Sequences` from a counter node | Gemini | that is data a program keeps, not a catalog object |
| `EnumValues` and `ColumnStats` from distinct values and counts | Gemini | each reads the data, which D47 forbids |

No lead answers a kind. The full text analyzers and the remote database
aliases are the nearest, and D162 records why each is a stretch.

### What the fixture builds, and what it cannot build

`models/neo4j/fixture` builds in the database `dbmeta`, as the administrator.
The core tables are the labels author, book, region and shipment, each with a
node key, and nodes that carry them, because a label exists while a node
carries it. The relationship types written_by and ships_to join them. There
are a uniqueness constraint, two existence constraints, a property type
constraint, a relationship key, the core index `book_published`, an index on
two properties, a text index, a full text index on two labels, and an index on
a relationship type. A role `dbmeta_reader` has a grant and a denial, and the
ordinary user holds it and has a home database.

It cannot build a view, a foreign key, a check constraint, a default, a
comment, a trigger, a sequence or a type, because Neo4j has none. It cannot
build a user defined function or procedure, because each is a Java plugin in
the plugins directory of the server, so routines are read from the built in
ones with the system objects.

### What the conformance test says

The `neo4j` section holds the four core labels and nothing else, on both
releases. There is no column catalog, and the report compares no constraint
where it has no column, so `agreementExcluded` names Neo4j.

### Which answers depend on who is asking

Neo4j has no containment and no object owner. A user belongs to the server
and holds roles, so there is one lesser kind of principal. It is
`dbmeta_user`, which the `dbrun` setup makes with the role publisher, which
reads and writes and cannot manage the server.

`Roles`, `RoleGrants`, `RoleSettings` and `Privileges` are refused to the
user, because SHOW USERS, SHOW ROLES and SHOW PRIVILEGES need privileges that
publisher does not hold. `Settings` returns no rows to the user rather than a
refusal. `Functions` and `Aggregates` leave `access` absent for the user,
because SHOW FUNCTIONS and SHOW PROCEDURES report no roles to a user who
cannot see the roles. The parity scene asks for the schema `dbmeta`,
where a routine of the server has no row, so the file does not show that
difference. Both releases answer the same, and one section,
`neo4j/same/user`, holds them.

### The describe fields (D210)

Indexes reads `state`, `createStatement`, `indexProvider` and the `indexConfig` of `SHOW INDEXES` as `Valid`, `Definition`, `Using` and `Options`. The row count has no one statement source, because `db.stats.retrieve` needs a `db.stats.collect` call first. There is no owner, size or predicate.

## YDB

`models/ydb` answers 7 of the 65, measured on 2026-10-01 on 26.2.1.14 and
26.3.1.17 through ydb-go-sdk, which is the driver dburl names and usql uses.
YQL has no information_schema, so the model reads the views in the directory
`.sys` of the database, and nothing else. See D161.

### A path, not a catalog

YDB keeps its objects in a tree of directories. A database is a path, such as
/local, and a table is a path inside it, such as
/local/dbmeta/dbmeta_fixture/author. The catalog is the path of the
database. A schema is a directory, named by its path relative to the
database, such as dbmeta/dbmeta_fixture, and a nested directory is a schema
of its own. A table at the root of the database has the empty schema.

### What it answers

Databases, schemas, tables, tablespaces, roles, role grants and privileges.

`auth_owners` holds every path with its owner and nothing that says what the
path is. `partition_stats` holds every partition of every table, so a table
is a path in both. The tables that implement an index are in
`partition_stats` and not in `auth_owners`, so the join leaves them out. A
directory is a path that holds another path. The database is the shortest
path, because it is a prefix of every other.

A tablespace is a storage pool from `ds_storage_pools`, which is where a
database keeps its data. A column family of a table names the kind of pool,
such as ssd or hdd, and the options carry the kind and the erasure.

A role is a user from `auth_users` or a group from `auth_groups`. A user can
log in while it is enabled, and a group never can. A role grant is a row of
`auth_group_members`. A privilege is the explicit grants on a path from
`auth_permissions`, as sid=permission, and the type of the path is table,
directory, or object for a path no view names.

### Two answers that are partial, and say so

Tables lists no view, and Schemas lists no empty directory. A view, a topic,
an empty directory and every other kind of object is a path with no
partitions and no children, and no view tells them apart. The part each
returns is exact, the field description says what is missing, and
`TestYDBFixtureObjects` builds a view and an empty directory and asserts
that neither is listed. D161 amends D45 for these two.

Every table reads table, and a column table does too. `hive_tablets` tells a
column shard from a row shard, and a new column table reports tablet 0 in
`partition_stats` for about a minute, measured on 26.3, so the join gives a
wrong answer for that minute.

### What it cannot answer, and why

49 kinds. The schema of an object holds its columns, its indexes, its key,
its changefeeds and its column families, and YDB gives it only to a gRPC call
per object, DescribeTable and its relatives. No SELECT reaches it, and D146
allows a walk for Impala alone. So columns, indexes, index columns,
constraints, constraint columns, views, sequences and partitioned tables are
unanswered although YDB has every one of them. A Serial column makes a
sequence, and `hive_tablets` lists its sequence shard without a path.

Indexes are the closest. The name of each index and its table are exact, from
the path of the table that implements it, `<table>/<index>/indexImplTable`.
Nothing says whether the index is unique, and `Index.Unique` is a plain bool,
so a unique index would read as not unique.

YQL has no catalog of functions, types, collations or settings, and YDB has
no trigger, no comment, no extension, no domain and no foreign key. YDB has
external data sources, external tables and asynchronous replications, and no
`.sys` view names one, so foreign servers, foreign tables and subscriptions
are unanswered. The fixture builds none of them. A changefeed is not even a
path in `auth_owners`, measured on 26.3.

The current user is unanswered. `CurrentAuthenticatedUser()` returns the
empty string for root and for dbmetauser. The current schema is unanswered,
because a session has no current directory and `PRAGMA TablePathPrefix`
holds for one statement and nothing reads it back.

PostgreSQL syntax has a `pg_catalog`, and the server refuses it with
"PostgreSQL syntax is not supported" unless a feature flag is set, which a
consumer cannot count on.

### A fault in the server

A read of a `.sys` view fails with an internal error, "requirement
!Meta->GetReads()[0].GetKeyRanges().empty() failed", when its filter is false
before any row is read, measured on 26.3. `WHERE $p0 = 'x'` fails and
`WHERE $p0 = 'x' OR Path IS NULL` returns no rows. The types filter of
Tables names no column of its own, because every table has one type, so it
also tests the name, which is never absent.

### What a second opinion found

Gemini and DeepSeek were asked about the 49 kinds on 2026-10-01, as hard rule
14 requires. DeepSeek answered after several attempts timed out, and named
nine views: `.sys/columns`, `indexes`, `views`, `sequences`, `changefeeds`,
`topics`, `external_tables`, `settings` and `current_user`. None exists. Each
was read on 26.3 and each failed with "Cannot find table". So did `tables`,
`pg_tables` and `pg_class`, which a PostgreSQL compatible catalog would
have.

Gemini said that no `.sys` view lists any of them and named two leads. The
first reads the columns of one table from the type of `TableRow()`. It
answers one table per statement, and only a table that holds a row, which is
a walk. The second finds the current user in `.sys/query_sessions`, whose
`UserSID` names the user of each session. It holds, and the only way to find
this session's row is to match the text of the statement, so two connections
that ask at once can each read the other's row. Both are left unanswered.

### What the fixture builds

`models/ydb/fixture` builds the directory dbmeta/dbmeta_fixture, because the
user dbrun makes can read and describe dbmeta. It holds author, book, region
and shipment, with a NOT NULL column, a default, a composite key, a Serial
column and three global indexes, one of them unique, and the view recent.
YDB has no foreign key, no check and no unique constraint. It also builds a
column table in two partitions, a topic, a changefeed, an empty directory,
a user, a group, a membership and a grant.

YQL has no statement that makes or removes a directory. A table makes one,
and the empty directory is made by making a table in it and dropping it. The
teardown leaves the directories behind.

The year of A Wizard of Earthsea is 1968, and a Date holds nothing before
1970, so book.published is a Date32.

### What the conformance test says

The section holds the four tables and nothing else. There is no column
catalog and no constraint catalog, and recent is a view, which Tables does
not list. YDB is in `agreementExcluded` for that reason, and it agrees with
every other database on the four lines all of them share.

### Which answers depend on who is asking

Every one. Every `.sys` view refuses a user that is not an administrator,
with "Cannot find table ... because it does not exist or you do not have
access permissions". dbmetauser can read and describe /local/dbmeta and is
refused all seven, and the parity file records each refusal.

### The describe fields (D210)

Tables reads the owner from `.sys/auth_owners`, and the size and the rows from `.sys/partition_stats`, summed over the partitions of a table. YDB updates the figures about every half minute, so a new table reads 0 for a while, and the size leaves out the index tables.

## ArangoDB

`models/arangodb` answers 7 of the 65 on ArangoDB 3.12.12, measured on
2026-10-01 and 2026-10-02 through dbimp's arangodb driver, which is what
dburl's arangodb scheme opens and what usql uses. ArangoDB is queried in AQL
rather than SQL, and it has no relational catalog. D163 holds the mapping,
and D168 holds what Ken changed in it.

### What AQL reads

The model reads what one AQL statement reads for the whole database:
`COLLECTIONS()`, `SCHEMA_GET`, the system collection `_aqlfunctions`, and the
functions `CURRENT_DATABASE()` and `CURRENT_USER()`. The HTTP API describes
much more, and it does so one call for each collection where it goes deeper.
Ken chose on 2026-10-01 that a walk (D146) is not allowed for ArangoDB, so a
kind that only the HTTP API answers is not answered.

A database is the schema, and the catalog is empty, as on MySQL and
ClickHouse. A connection names one database in its path and AQL cannot reach
another, so Schemas and CurrentSchema return the database of the connection,
from `CURRENT_DATABASE()`. Ken chose this on 2026-10-02 (D168). D163 first
made the database the catalog, with no schema, as on Firebird.

### What it answers

Schemas, the current schema, tables, columns, constraints, functions and the
current user.

A collection is a table, of the type `collection`. `COLLECTIONS()` has no
type, so it does not say whether a collection holds documents or edges.
`with_system` adds the collections whose names start with `_`.
`Table.Rows` is the exact count of documents, from `COLLECTION_COUNT(name)` in
the same statement that lists the collections (D212). A cluster counts with a
round trip for each shard, and no cluster was measured.

A document has no fixed shape, and ArangoDB keeps no catalog of the
attributes of a collection's documents. Ken chose on 2026-10-01 that D47's
cost test is strict here, as it is for Neo4j, so an attribute that only the
documents show is not a column. A column is a top-level property of the
collection's JSON schema rule, which `SCHEMA_GET` reads from the collection's
properties, in the order the rule lists it. `data_type` is the JSON schema
type, `nullable` is false where the rule requires the attribute and its type
does not take null, and `primary_key` is true for `_key`. A rule checks a
document as its level says, and it does not check a document stored before
it was set, so the field description says that a stored document can still
lack a required attribute.

A schema rule is also one constraint of the type `check`, with no name,
whose definition is the whole schema as JSON. The server refuses a document
that breaks it with error 1620.

A function is a user defined AQL function from `_aqlfunctions`, with its
namespace in its name, such as `dbmeta::full_title`. Its volatility is
`immutable` where it was registered as deterministic and `volatile`
otherwise, and its source is the JavaScript the server keeps. The built in
functions are in no collection AQL reads, so `with_system` adds none.

The version is `RETURN VERSION()`, which is also usql's statement.

### The cost

On a database of 2000 collections, each with a rule of three properties, the
columns query returned 6000 rows in 11 milliseconds and the tables query
2000 rows in 4 milliseconds, as the server timed them. The plan of the columns
query reads no collection: it is an enumeration over the list
`COLLECTIONS()` returns and a calculation for each item. Measured on
2026-10-01, in a database made for it and then dropped.

### What it cannot answer, and why

51 kinds.

- Indexes, index columns, views and databases. Only the HTTP API lists them.
  `INDEXES()`, `VIEWS()`, `DATABASES()` and `COLLECTION_TYPE()` are each
  error 1540, an unknown function.
- Roles, role grants and privileges. The users and their grants are in
  `_users` in `_system`, which AQL in another database cannot read and the
  ordinary user cannot reach.
- Routine parameters. A function's parameters are only in its JavaScript
  source.
- Constraint columns. A rule checks the whole document.
- Sequences, triggers, types, domains, comments and the rest are absent from
  ArangoDB. A collection's key generator is a property that AQL does not read.

### What a second opinion found

Gemini, as gemini-3.1-pro-preview because gemini-3.8-flash closed the
connection, and DeepSeek, as deepseek-flash, were asked about the
unanswered kinds on 2026-10-01, as hard rule 14 requires. Both said that AQL
lists no index and no view, that it cannot tell an edge collection from a
document collection, and that `_graphs` names only the edge collections a
graph uses. Each lead was run against 3.12.12:

- `CURRENT_DATABASE()` for the current schema, from both. It holds under the
  other mapping, where a database is a schema, and D163 sets that out for
  Ken. Under the mapping the model uses, there is no schema.
- `_apps` for extensions, from both. A Foxx service is an application
  mounted at a path, and it adds no object to the database. A stretch.
- An `enum` in a rule for enum values, from both. It is a check on one
  property, not a type with labels. A stretch.
- `SCHEMA_GET` for constraints, from Gemini. It holds, and the model answers
  it. Gemini also named the property keys of a rule for constraint columns,
  which is a stretch, because a rule checks the whole document.
- `_analyzers` for text search configurations, and for collations, from
  Gemini. An analyzer of the type `text` splits, folds and stems, so it is a
  configuration, a parser and a dictionary at once, which is why CrateDB's
  analyzers were left out (D131). `_analyzers` also holds only the analyzers
  the database defines: the HTTP API listed 14 where `_analyzers` held 1,
  because the 13 built in analyzers are in no collection. An analyzer's
  locale is not a collation. Neither is answered.
- A regular expression over a function's source for routine parameters, from
  Gemini. JavaScript allows a default value, an arrow function and a rest
  parameter, so a pattern is a guess rather than a catalog read. Not
  answered.

A graph fits no kind. Its edge definitions look like foreign keys, and
ArangoDB does not enforce them on an AQL write: an edge from `book` to
`author` in the graph `authorship`, defined from `author` to `book`, was
accepted.

### What the fixture builds

`models/arangodb/fixture` builds in the database `dbmeta`. The driver makes
the collections and the index, and the HTTP API makes everything else,
because AQL has no DDL. The core collections author, book, region and
shipment each have a strict rule whose properties are the core columns.
`note` has a rule with `_key`, a property with no type, a type that is a
list, and a required property that takes null, at the level `new`. `loose`
has no rule. There are two functions, one deterministic.

It also makes the core view `recent`, the index `book_published`, the edge
collection `wrote`, the graph `authorship` and the analyzer `dbmeta_text`,
which no query reads. `TestArangoDBFixtureObjects` checks that `recent` is not
a table and that `wrote` is a collection, so each gap stays a decision.

### What the conformance test says

The four core collections and their columns, with every NOT NULL where the
other databases have one. No column is a primary key, because a rule names
no key, and there is no view and no constraint line, so ArangoDB is in
`agreementExcluded` with that reason.

### Which answers depend on who is asking

`dbmeta_user`, with read and write on the database, gets the
administrator's answer to every query but the current user. The parity test
also makes `dbmeta_reader`, with read only access to the database and none
to the collection `note`. `COLLECTIONS()` leaves out a collection the user
cannot access, so the reader gets fewer tables, columns and constraints.
`_aqlfunctions` answers both users the same.

## InfluxQL

`models/influxql` answers 7 of the 65 on InfluxDB 1, measured on 2026-10-01
on 1.13.1 and 1.11.8 through dbimp's influxdb driver, which is what dburl's
influxql scheme opens and what usql uses. InfluxDB 2.9.1 and 2.8.0 answer 4
of the 7 through their v1 API, and so does the InfluxQL of InfluxDB 3 Core.
InfluxQL reads metadata only through SHOW statements, and each one reads one
database, so most kinds are a walk (D159). D165 holds the mapping.

### What it answers

| Kind | InfluxQL | Statements | Releases |
| --- | --- | --- | --- |
| Schemas, Databases | each database, from SHOW DATABASES | one | 1, 2 and 3 |
| Tables | each measurement, from SHOW MEASUREMENTS ON | one, and one for each database | 1, 2 and 3 |
| Columns | time, and each tag and field, from SHOW FIELD KEYS ON and SHOW TAG KEYS ON | one, and two for each database | 1, 2 and 3 |
| Roles | each user, from SHOW USERS | one | 1 |
| Privileges | each database with every grant on it, from SHOW GRANTS FOR | one, and one for each user | 1 |
| Settings | the configuration, from SHOW DIAGNOSTICS | one | 1 |

A database is the schema, and the catalog is empty. A measurement is a table.
Its columns are time, with the type `timestamp`, each tag, with the type
`tag`, and each field, with its InfluxQL type: float, integer, unsigned,
string or boolean. The ordinal is the position in the answer of SELECT *:
time first, then every tag and field by name. Time is the only column that
is NOT NULL.

On InfluxDB 2, SHOW DATABASES lists the buckets, because InfluxDB 2 maps a
database named for each bucket to it. `_internal`, `_monitoring` and `_tasks`
are system objects.

No InfluxQL statement names the release. Only `GET /ping` does, so
`dbmeta.InfluxQL.Version` reports an unknown version. A caller that reads
the release from the driver passes it to `dbmeta.InfluxQL.ParseVersion`, and
usql already reads it from there. With an unknown version, Roles, Privileges
and Settings report Supported on every release, and InfluxDB 2 and 3 refuse
each one: InfluxDB 2 answers "not implemented", and InfluxDB 3 does not parse
the statement.

### What it cannot answer, and why

49 kinds. Most are absent from InfluxDB: constraints, triggers, sequences,
functions, types, collations and comments among them. InfluxQL has functions
such as MEAN, and no statement lists them.

`CurrentSchema` and `CurrentUser` are not answered. The request carries the
database and the user, and no statement returns either.

Five kinds have something close, and each is a stretch. A retention policy
is close to a tablespace, and it belongs to one database, so every database
has its own `autogen`. A continuous query is close to a view, and no
statement selects from it by its name. An InfluxDB subscription sends each
write to an outside address, which is the opposite of a PostgreSQL
subscription. SHOW TAG VALUES CARDINALITY counts the values of one tag across
every measurement, which is a statement for each tag, and no count for a
field leaves the points alone. SHOW SERIES CARDINALITY counts series and is
not an object. D165 has each reason.

### The cost of reading the columns

D47 leaves a kind unsupported when its only source reads the points. SHOW
FIELD KEYS and SHOW TAG KEYS read the index. On 1.13.1, both took 0.0004
seconds with 1,000 points and with 5,000,000 points, where SELECT count took
0.117 seconds over the same points. SHOW TAG KEYS took 0.011 seconds with
100,000 series and 0.122 seconds with 1,000,000 series on InfluxDB 1, whose
default index is in memory, and 0.0013 seconds with 1,100,000 series on
2.9.1, whose index is on disk. So neither reads the points, and SHOW TAG KEYS
on InfluxDB 1 grows with the index. D165 has the table.

### What a second opinion found

Gemini and DeepSeek were asked about the 49 kinds on 2026-10-01, as hard rule
14 requires. Each lead was run against 1.13.1, 2.9.1 or 3.11.5.

Both named SHOW SUBSCRIPTIONS for subscriptions, and both named SHOW TAG KEYS
for index columns, because the index of series holds every tag. A
subscription sends writes to an outside address, and the index of series is
not an object that a statement makes, so both are stretches. DeepSeek named
SHOW CONTINUOUS QUERIES for views, and Gemini called a continuous query a
materialized view and named no kind for it. No statement selects from a
continuous query, so it is not a view. Gemini named the cardinality
statements for column statistics and SHOW SERIES CARDINALITY for extended
statistics. Measured on 1.13.1, SHOW TAG VALUES CARDINALITY counts the values
of one tag across every measurement, and the cardinality statements do not
exist on InfluxDB 2. DeepSeek named SHOW GRANTS for role grants, and a grant
is a privilege on a database rather than a membership of a role, so
Privileges answers it. DeepSeek named SHOW DIAGNOSTICS for the version, which
answers only an administrator on InfluxDB 1, and InfluxDB 2 and 3 do not have
it. Both put every other kind in the absent group, and none of their answers
named a source that this section does not.

### What the fixture builds

`models/influxql/fixture` writes four measurements, author, book, region and
shipment, with the INSERT that dbimp's driver turns into line protocol, and a
tag, an integer, a float, a string and a boolean among their columns. It is
the InfluxDB 3 fixture, in the database dbmeta. It makes no user, no grant
and no database, because InfluxDB 2 and 3 refuse CREATE USER, GRANT and
CREATE DATABASE, and no statement names the release, so a step for
InfluxDB 1 alone cannot be skipped elsewhere. The dbrun entry makes the user
`dbmeta_user` with READ on dbmeta on InfluxDB 1, and Roles and Privileges
read it.

### What the conformance test says

The section is the same on InfluxDB 1, 2 and 3: the four measurements and
their columns. It agrees with InfluxDB 3's section in every line but the
ordinal, because InfluxDB 3 counts time last and SELECT * puts it first.
InfluxQL is out of the agreement count for the reason InfluxDB 3 is.

### Which answers depend on who is asking

On InfluxDB 1, Roles, Privileges and Settings are refused to `dbmeta_user`,
who can read dbmeta and nothing else: SHOW USERS and SHOW DIAGNOSTICS need an
administrator. The other four give the administrator's answer, because SHOW
DATABASES lists only what a user can read and the fixture is in dbmeta. On
InfluxDB 2 the user is a v1 authorization that can read the bucket dbmeta,
and every answer agrees, because the three kinds are refused to the
administrator too. InfluxDB 3 Core has no user with fewer rights than the
administrator, so the test skips it.

## SurrealDB

`models/surrealdb` answers 18 of the 65 on 3.1.6, 3.2.4 and 3.3.0, and 1 on
2.7.0, measured on 2026-10-01 through dbimp's surrealdb driver at v0.10.0,
which is what dburl's surrealdb scheme opens and what usql uses. 2.7.0 and
3.3.0 are Tested, and 3.1.6 and 3.2.4 are Nightly. D164 holds the mapping
and the reasons for it, for Ken to review.

### It reads INFO as a value

SurrealDB has no relational catalog. INFO FOR ROOT, INFO FOR NS, INFO FOR DB
and INFO FOR TABLE return the namespaces, the databases, the tables, the
functions, the sequences, the users, the fields, the indexes and the events,
and with STRUCTURE each one is an object with its parts. From 3.0 an INFO
statement is a value that a SELECT reads. `INFO FOR TABLE $t.name` takes its
table from a variable, so `array::map` over the tables reads INFO FOR TABLE
for each table inside one statement. No kind is a walk, which Ken ruled out
for SurrealDB on 2026-10-01.

A namespace is the catalog and a database is the schema. Every kind below a
schema reads the database the connection is in, which the path of the URL
names, so a pattern that names another database matches nothing.

Five things about writing SurrealQL for it, all measured:

1. On 2.7.0 INFO is never a value. `RETURN (INFO FOR DB)`, `LET $i = INFO FOR
   DB` and `SELECT * FROM (INFO FOR DB)` are parse errors. So 2.7.0 answers
   only the current schema, from `session::ns()` and `session::db()`, and
   reports `TooOld` for every other kind.
2. The server sorts the keys of every object it returns, and the driver takes
   the columns from the keys. So every query declares its fields in the order
   of their names.
3. SurrealQL has named parameters only. The dialect sets `Info.Named`, so a
   parameter is `$p1`, `$p2` and so on, bound with `sql.Named`.
4. SurrealQL has no LIKE. A pattern is turned into a regular expression with
   `array::fold`. `TestSurrealDBPatterns` checks it against `dbmeta.Like`, with
   `.`, `*`, `(`, `[`, `$`, `^`, `#`, `<`, a space, a trailing backslash and an
   escaped `_` among the patterns. 3.x names the string tests
   `string::is_alphanum` and `string::is_ascii`.
5. 3.1 refuses an ORDER BY on a field the SELECT does not return, and 3.3
   takes it. The server also refuses a statement whose expressions nest too
   deep, which ConstraintColumns reached once, so the two constraint kinds
   match the table pattern in the WHERE.

The cost was measured on 3.3.0 with 2000 tables of 5 fields each. Tables
took 0.02 seconds. Columns took 0.34 seconds for all 10000 fields, and 0.08
seconds for one table, because a kind below a table reads INFO FOR TABLE only
for the tables that match its table pattern.

### What it answers

Databases, schemas, the current schema, tables, views, columns, indexes,
index columns, constraints, constraint columns, triggers, functions, routine
parameters, sequences, roles, role grants, privileges and comments, on 3.x.
The current schema on 2.7.0.

Eight answer with an analogue:

| Kind | SurrealDB | Why it is a fair answer |
| --- | --- | --- |
| `Databases` | namespaces | a namespace holds databases as a PostgreSQL database holds schemas |
| `Schemas` | the database of the connection | each kind below reads only that database, so it is the only schema listed (D168) |
| `Columns` | a DEFINE FIELD | the other fields of a schemaless table are known only from the records, which D47 forbids. Ken chose this for Neo4j on 2026-10-01 |
| `Constraints` | a UNIQUE index, and the ASSERT of a field as a check | both refuse a write, and an ASSERT takes the name of its field |
| `Triggers` | a DEFINE EVENT | it runs when a record of its table changes |
| `Roles` | system users on the root, the namespace and the database | a user signs in and holds a role. OWNER on the root is the superuser |
| `RoleGrants` | the roles OWNER, EDITOR and VIEWER that each user holds | the three roles are fixed, and a user holds them at its level |
| `Privileges` | the PERMISSIONS of a table and of its fields | they say what a record user can do. D164 asks Ken to confirm it |

A view is a table defined AS SELECT, and a table defined TYPE RELATION has
the type `relation`. The type of a column is its TYPE, and `any` where it has
none. A field with a VALUE clause reads `generated` `s`, and a COMPUTED field
reads `v`. The record id is the key of every table, and a column is
`primary_key` only when a DEFINE FIELD names `id`. SurrealDB records no
position for a field, so `ordinal` is the place in the order of the names.

### What it cannot answer, and why

38 kinds on 3.x, and 55 on 2.7.0.

`CurrentUser` is absent. No function names the system user a session signed
in as, `$auth` is NONE for a system user, and `$session` names no user,
measured on 3.3.0. `TestSurrealDBCurrentUserIsNotSupported` holds it.

Six more are close and rejected, which D164 lists with the reasons:
`Settings` from DEFINE PARAM and DEFINE CONFIG, `TextSearchConfigs` from
DEFINE ANALYZER, `Extensions` from the modules and the models, `EnumValues`
from a union of literals, `AccessMethods` from the kinds of index, and
`ForeignServers` and `ForeignTables` from DEFINE API and DEFINE BUCKET. A
foreign key from REFERENCE is rejected too, because a field with REFERENCE
took a link to a record that does not exist.

The rest are absent from the product: types, domains, collations, casts,
conversions, operators and their classes and families, languages, large
objects, event triggers, tablespaces, partitioned tables, publications,
subscriptions, foreign data wrappers, user mappings, default privileges,
extended statistics, column statistics, role settings, aggregates and the
other text search kinds.

### What a second opinion found

Gemini and DeepSeek were asked about the kinds it cannot answer on
2026-10-01, as hard rule 14 requires. Each lead was run against 3.3.0.

| Lead | Who | What the server says |
| --- | --- | --- |
| `session::user()` for the current user | DeepSeek | a parse error, "Invalid function/constant path". Gemini said no function names a system user, which is right |
| `Types` and `Domains` from an INFO FOR DB key types | DeepSeek | INFO FOR DB has no such key on 3.3.0 |
| `Extensions` from an INFO FOR DB key plugins | DeepSeek | no such key. The keys are modules and models, which D164 rejects |
| `Settings` from params on the root, the namespace and the database | Gemini | INFO FOR ROOT and INFO FOR NS list no params. INFO FOR DB lists them, and a param is a value and not a setting |
| `Settings` from DEFINE CONFIG | Gemini | it holds the GraphQL setup, such as `{graphql: {functions: AUTO, tables: AUTO}}`, which is the shape of an interface |
| `Privileges` from the PERMISSIONS of tables and fields | both | answered: INFO lists them for select, create, update and delete |
| `TextSearchConfigs`, parsers and dictionaries from the analyzers | both | an analyzer has tokenizers and filters and no parser or dictionary. A stretch |
| `EnumValues` from a union of literals | both | it is the type of one field and not a named type. A stretch |
| a foreign key from REFERENCE | Gemini | REFERENCE ON DELETE REJECT took `rbook:missing`, so it does not check that a link exists |
| `ForeignTables` from DEFINE API and DEFINE BUCKET | Gemini | an API is an endpoint the server serves and a bucket is a store for files |
| `AccessMethods` from the kinds of index | Gemini | a fixed set that no catalog lists. Indexes returns the kind of each index |
| INFO as rows on 2.x | both said no | right: every form is a parse error on 2.7.0 |

One lead answers a kind, Privileges, which was already planned. The others
are absent or a stretch.

### What the fixture builds, and what it cannot build

`models/surrealdb/fixture` builds in the database `dbmeta` of the namespace
`dbmeta`, as the root user. author, book, region and shipment are SCHEMAFULL
tables with DEFINE FIELD columns: a field that is not null, an optional one,
one with a default, one with a VALUE clause and, on 3.x, a COMPUTED one.
There are an ASSERT on book.title, a UNIQUE index on book.title, the core
index `book_published`, an index on two fields, a UNIQUE index on
region.country and region.area, which stands in for the composite key, and a
full text index. recent is a table defined AS SELECT, wrote is a TYPE
RELATION table, and region carries PERMISSIONS on the table and on a field.
There are an event, a function with two parameters and a result type, a
param, an analyzer and, on 3.x, a sequence. Several carry a COMMENT.

It cannot build a foreign key or a composite key, because SurrealDB has
neither, and it cannot build a type, a domain, a collation or a cast. The
ordinary user and the root user are the dbrun setup's.

### What the conformance test says

The `surrealdb` section holds the five core tables, their columns and three
constraints, on 3.x. No column reads `primary_key`, because no DEFINE FIELD
names `id`, there is no foreign key, and the ordinal is the place in the order
of the names. So `agreementExcluded` names SurrealDB. 2.7.0 is skipped,
because it answers the current schema alone.

### Which answers depend on who is asking

SurrealDB defines a system user on the root, on a namespace or on a database,
with one of three roles there. The administrator is the OWNER on the root.
The parity test asks as three lesser principals: `dbmeta_user`, the EDITOR on
the database that the dbrun setup makes, a VIEWER on the database, and an
EDITOR on the namespace. A record user signs in through a DEFINE ACCESS, and
the driver signs in system users only, so it is not measured.

`Databases`, `Roles` and `RoleGrants` read INFO FOR ROOT, which only a user on
the root reads, so all three lesser principals are refused them. `Schemas`
reads INFO FOR NS, which a user on the database is refused and a user on the
namespace reads. Every other kind gives the administrator's answer to all
three, because a user on a database reads INFO FOR DB and INFO FOR TABLE.
The three 3.x releases answer the same, and 2.7.0 answers only the current
schema, which every principal reads. The sections `surrealdb/same/user`,
`surrealdb/same/viewer` and `surrealdb/same/namespace` hold them.

## Apache Druid

`models/druid` answers 7 of the 65 on Apache Druid 37.0.0 and 38.0.0, measured
on 2026-10-07 through dbimp's druid driver, which is what dburl's druid scheme
opens. Both releases are Tested. Druid answers SQL on the Router with Apache
Calcite, and Calcite keeps an INFORMATION_SCHEMA and Druid adds a sys schema.
D171 holds the mapping and the reasons for it, for Ken to review.

### What it reads

INFORMATION_SCHEMA has four tables: SCHEMATA, TABLES, COLUMNS and ROUTINES. It
has no PARAMETERS and no VIEWS. The sys schema has the segments, the servers,
the server properties, the server segments, the supervisors and the tasks.
Every query is one statement over one of them, and no kind is a walk (D146).

Druid has no DDL. A datasource appears when an ingestion task writes its first
segment, and the task API takes INSERT and REPLACE only. So Druid has no
index, constraint, trigger, sequence, type, domain, comment or view that a
statement makes.

### What it answers

Schemas, the current schema, tables, columns, functions, aggregates and
settings.

A datasource is a table in the schema druid, and each one has the column
`__time`, which is the only column that is NOT NULL. The other schemas are
INFORMATION_SCHEMA and sys, which hold the server's own tables and are listed
only with the system objects, and lookup and view, which are empty until
somebody makes a lookup or a view. A column reads the SQL type of the
datasource, such as BIGINT, VARCHAR or TIMESTAMP, and a column written as
a timestamp other than `__time` is stored and read as a BIGINT. COLUMN_DEFAULT
is the empty string for every column, so the model reads it as absent.
COLLATION_NAME is the collation of the Calcite planner for a string column,
which is not a setting of Druid, and the model reports it as the catalog
gives it.

There is no function that returns the current schema. Druid resolves a name in
druid, and SCHEMATA names it.

ROUTINES has one row for each function name, 226 of them on both releases, and
41 are aggregates. Druid has no user defined function, so each one is built
in and is listed only with the system objects. The row has no return type and
no parameter table, so SIGNATURES, which is text with one overload on each
line, is the field `arg_types`. It is not parsed.

Settings read `sys.server_properties`, which holds the runtime properties of
every service, one row for each property of each service. A name repeats, and
the context says which service holds it. 38.0.0 adds the column
error_message to that table, and the model does not read it. The driver reads
a value that is a JSON array of two or more strings, such as the list of
extensions, as a list, and the model writes it back as compact JSON.

The version is `sys.servers.version` of the broker, such as 37.0.0. No Druid
function returns the release, and `GET /status` is the only other source.

### What it cannot answer, and why

49 kinds. Most are absent from Druid.

`CurrentUser` has no source. `CURRENT_USER` and `USER` are columns that do not
exist in Druid SQL, measured on 37.0.0. The security API of the Coordinator
holds the users and the roles, and it is an HTTP API that no SQL statement
reaches, so roles, role grants and privileges are not answered.

`Databases` is not answered. Druid has one catalog, named druid, and SCHEMATA
lists schemas and not databases.

`Views` is not answered. TABLES has a type VIEW, and Druid has no INFORMATION_SCHEMA.VIEWS
and no statement that returns a view's definition.

`RoutineParameters` is not answered. ROUTINES holds the signatures as text, and
a row such as `'APPROX_COUNT_DISTINCT_DS_HLL(column, lgk)'` has no stable form
to read the names and the types from.

### What a second opinion found

Gemini and DeepSeek were asked on 2026-10-07, as hard rule 14 requires. Gemini
answered the whole list in two calls. DeepSeek spent its whole budget on
reasoning in three tries, and timed out in one, and answered a short question
on the fifth try, with a budget of 30000 tokens. Each lead was run against 37.0.0.

| Lead | Who | Result |
| --- | --- | --- |
| Databases from `SCHEMATA.CATALOG_NAME` | Gemini, DeepSeek | A stretch. There is one catalog, and SCHEMATA lists schemas. Not answered |
| Types from `SELECT DISTINCT DATA_TYPE FROM COLUMNS` | Gemini, DeepSeek | A stretch. It lists the types that a column uses, which is a scan of the columns and not a type catalog (D162). Not answered |
| Partitioned tables from `sys.segments` | Gemini, DeepSeek | A stretch. Every datasource is partitioned by time, and the segment granularity is not in the catalog. A segment row has a start, an end and a partition number, and the statement grows with the segments and not with the tables. Not answered |
| Views from `INFORMATION_SCHEMA.TABLES` with the type VIEW | Gemini | Held in part. TABLES has the type, and the Tables query reports it. There is no definition, so Views is not answered |
| The current user from `CURRENT_USER` | Gemini | Wrong. Druid refuses it: Column 'CURRENT_USER' not found in any table |
| Extensions from the property `druid.extensions.loadList` | the author | A stretch. It is one JSON text for each service, and a statement cannot split it. Not answered |

Both models called every other kind absent, and the survey agrees.

### What the fixture builds

`models/druid/fixture` makes the four core datasources, author, book, region
and shipment, with a REPLACE of the multi-stage engine through
`POST /druid/v2/sql/task`, which a test sends over HTTP because the driver
reads and never writes. A task takes from 5 to 12 seconds and the nano
quickstart has two slots, so the steps run one after the other and the setup
skips a datasource that already answers. Druid has no DROP in SQL, so the
fixture has no teardown. It cannot build an index, a constraint, a view or a
function, and `TestDruidLeavesOut` asserts that those kinds are not answered.

### What the conformance test says

Druid is out of the main agreement count. A datasource has no key, no
constraint and no view, so the section holds the four datasources and their
columns, with `__time` first and the only column that is NOT NULL.

### Which answers depend on who is asking

Druid filters INFORMATION_SCHEMA by permission. The ordinary user, who can READ
every datasource, sees what the administrator sees in schemas, tables,
columns and functions. A reader who can READ the datasource author alone sees
that datasource and no other, so tables and columns return fewer rows.

Settings read `sys.server_properties`, and the version reads `sys.servers`.
Both need the permission STATE, so Druid answers HTTP 403 with Insufficient
permission to view servers to the ordinary user and to the reader. The
settings query is refused, and so is the version query. A caller that has no
version cannot build the metadata, so the administrator reads the version
and passes it. `TestDruidVersionRefusedToAnOrdinaryUser` asserts the refusal.

38.0.0 answers the same as 37.0.0 for all seven kinds, and for both principals.

### The describe fields (D210)

Tables reads `IS_JOINABLE` and `IS_BROADCAST` as the options. The size and the rows are NULL: `sys.segments` holds them, and a read of it covers every segment of the cluster for one table, which D47 does not allow.

## Apache Drill

`models/drill` answers 10 of the 65 on Apache Drill 1.21.2 and 1.22.0,
measured on 2026-10-08 through dbimp's drill driver, which is what dburl's
drill scheme opens. Both releases are Tested. Drill answers SQL with Apache
Calcite, and it has an INFORMATION_SCHEMA and a sys schema. D178 holds the
mapping and the reasons for it, for Ken to review.

### What it reads

INFORMATION_SCHEMA has CATALOGS, SCHEMATA, TABLES, COLUMNS, VIEWS, PARTITIONS
and FILES. The sys schema has the options, the functions, the version, the
Drillbits, the memory, the threads, the connections, the profiles and two
tables of aliases. Every query is one statement over one of them, and no kind
is a walk (D146).

Drill quotes a name with backticks. A double quoted alias fails with the state
FAILED and no message, so every alias in the model is between backticks.
`FILES` is a reserved word and needs backticks. A LIKE whose pattern is not a
constant fails the same way, and the driver writes each argument as a literal,
so the filters work. The planner of Drill takes the statistics columns of
COLUMNS as never NULL and folds `IS NOT NULL` to true, so the statistics query
tests `NUM_NULLS >= 0`.

### What it answers

Schemas, the current schema, tables, columns, views, databases, functions,
column statistics, settings and the current user.

The current schema has a row only when the session names a default schema,
with `schema=` in the DSN or with `USE`. CURRENT_SCHEMA is the empty string
otherwise, and the model returns no row and does not invent one.

### The Metastore

This is the price Ken accepted. INFORMATION_SCHEMA lists no file table by
default. A Parquet table that CREATE TABLE AS made does not appear. A file
table appears only when the Drill Metastore is on, which is the option
`metastore.enabled`, and only after `ANALYZE TABLE ... REFRESH METADATA` ran
for it. A model cannot set the option. If the Metastore is off, Tables lists
the views and the system tables and no file table, and Drill gives no error.
`TestDrillMetastoreOff` asserts that. A table that was analyzed and then
dropped stays listed until `ANALYZE TABLE ... DROP METADATA` removes it, which
the fixture does first.

The columns of a view read the type ANY and nullable, because Drill knows no
type until the view runs. The statistics of ColumnStats come from the same
analysis, so they exist only for an analyzed table. The minimum and maximum of
a DATE column read as the milliseconds since 1970-01-01. NDV is NULL on both
releases, and `EST_NUM_NON_NULLS` is NULL too.

### What it cannot answer

46 kinds. Drill has no index, constraint, key, trigger, sequence, comment,
type, domain or role that SQL lists, and a user defined function needs a JAR
file that a statement cannot make. Its users come from the authenticator in
the configuration. Its storage plugins are in `GET /storage.json`, an HTTP API
for an administrator, and not in a table.

`Aggregates` is not answered. sys.functions has no marker for an aggregate.
`RoutineParameters` is not answered. The signature is the text
`BIGINT-OPTIONAL,VARCHAR-REQUIRED`, which has no name, no position and no
identifier for the overload. `PartitionedTables` is not answered. PARTITIONS has
no rows. `Comments` is not answered. Drill has no COMMENT statement.

### What the second opinions found

| Lead | From | Result |
| --- | --- | --- |
| sys.storage_aliases as foreign servers | DeepSeek and the survey | Wrong. An alias names a storage plugin and is not a server. `CREATE PUBLIC ALIAS zzs FOR STORAGE dfs` works and the alias appears in sys.storage_aliases, but not in SCHEMATA or TABLES. A table alias was refused on 1.22.0 with no message. Not answered |
| an aggregate flag in sys.functions | DeepSeek | Wrong. The columns are name, signature, returnType, source and internal, and `FUNCTION_TYPE` does not exist |
| function parameters from sys.functions.signature | Gemini | A stretch. The text has no parameter name, no position and no overload identifier. Not answered |
| indexes, constraints, roles, privileges, comments and sequences | both | Absent, and the survey agrees |
| CATALOGS for the Databases kind | the survey | Held. It has one row, DRILL |

Gemini timed out on the first three questions and answered the fourth in
eight lines. DeepSeek spent its whole budget on reasoning until the question
was cut to one list and told to answer directly.

### What the fixture builds

`models/drill/fixture` turns the Metastore on as the administrator with
`ALTER SYSTEM`, makes the four core tables of D53 with CREATE TABLE AS in
dfs.tmp, makes the view recent, and runs ANALYZE for each table. The setup
drops everything first, so it is safe to run twice. It cannot build an index,
a constraint, a key, a trigger, a sequence, a comment, a type, a role or a
function. `TestDrillLeavesOut` asserts that those kinds are not answered.

### What the conformance test says

Drill is out of the main agreement count. A table has no key and no
constraint, so the section holds the four tables, the view and their columns.
The NOT NULL columns of the tables are an artifact of Parquet written from
literals, and the columns of the view are nullable.

### The cost check

Measured on 1.22.0 with 1500 views and 300 analyzed tables in dfs.tmp, which
is 1826 relations. TABLES answers in 0.2 seconds. COLUMNS answers in 2.2
seconds for every column and for the two columns of one table, because the
cost is Drill parsing each view, about 1.5 milliseconds for each, and a
filter does not prune it. The statistics statement adds a hash join with TABLES
and takes 2.7 seconds for all and 2.4 for one table. EXPLAIN shows the filter
pushed into the scan and a HashJoin, with no per row statement. The cost grows
with the number of views and not with the data. SCHEMATA and TABLES walk every
enabled storage plugin, so a slow plugin slows them, which was not measured
here because only file plugins exist.

### Which answers depend on who is asking

Nothing in the catalog. Drill filters INFORMATION_SCHEMA and sys by no user,
and the ordinary user, who can query and cannot change an option, sees the
rows of the administrator in every query. The current user differs, because
it names the user. Both users read the version from sys.version. The parity
file records one difference, `current_user`.

1.22.0 and 1.21.2 answer the same for all ten kinds. sys.functions has 31
more rows on 1.22.0.
### The describe fields (D210)

Tables reads `NUM_ROWS` as `Rows`, which is set only for a table that `ANALYZE TABLE` stored in the Metastore. There is no owner, size or options.

## Elasticsearch

`models/elasticsearch` answers 8 of the 65 on Elasticsearch 8.19.22, 9.4.6 and
9.5.3, measured on 2026-10-08 through dbimp's elasticsearch driver, which is
what dburl's elasticsearch scheme opens. 8.19.22 and 9.5.3 are Tested and 9.4.6
is Nightly. Elasticsearch answers SQL on `POST /_sql`. D177 holds the mapping
and the reasons for it, for Ken to review.

### What it reads

Its SQL has no table that a SELECT reads. It has SYS TABLES, SYS COLUMNS, SYS
TYPES, SHOW FUNCTIONS and SHOW CATALOGS, and none of them takes a WHERE or an
ORDER BY. So each kind but the current user is a walk (D175), and each walk is
one statement. The walk matches the patterns of the caller in Go.

SYS COLUMNS answers 1000 rows to a page and gives a cursor. The driver follows
the cursor, and closes it on the server when the caller stops. A catalog of
3000 indices and 24000 columns was read in 1.7 seconds, and the first row came
after 250 milliseconds. A `parent` pattern does not make it cheaper. SYS
COLUMNS can take a pattern, and then TABLE_NAME holds the pattern and not the
name of the index.

Elasticsearch has no DDL in SQL, so it has no index, constraint, trigger,
sequence or comment that a statement makes.

### What it answers

Databases, tables, views, columns, functions, aggregates, types and the
current user.

The cluster is the catalog and the one database. There is no schema, so every
schema is empty. An index is a table and an alias is a view, and so is a data
stream. A field is a column, and a subfield is a column of its own, such as
`name.raw` and `dims.h`. SYS COLUMNS leaves out an object, a nested field and a
field of the type dense_vector, flattened, a range or aggregate_metric_double,
and its ordinal counts the fields it leaves out. Every column is nullable and
none has a key, a default or a comment. The type is the name of the mapping type
in upper case.

A data stream is listed under its backing index in SYS COLUMNS. The backing index
is hidden and its name starts with a dot, so the model lists its columns, and
the table of the backing index, only with the system objects.

SHOW FUNCTIONS lists 161 names on 9.5.3 and 19 are aggregates. Every one is
built in, so they are listed only with the system objects. The type that
SHOW FUNCTIONS gives, SCALAR, CONDITIONAL, GROUPING or SCORE, has no field to go
in, so every function that is not an aggregate reads func. SYS TYPES lists 38
types. Elasticsearch gives no return type and no argument type of a function.

The version query is `SELECT version()`. The SQL of the server has no such
function, and dbimp's driver answers it from `GET /`, which holds
`version.number`. It works for every user, because the role of the ordinary user
holds the cluster privilege `cluster:monitor/main` (D191, D192).

### What it cannot answer, and why

48 kinds. Most are absent from Elasticsearch SQL.

Roles, role grants and privileges are in the security API. The restricted index
`.security-7` holds the native users and roles as documents, and no SQL
statement reads it. Settings are in the settings API of the cluster and of each
index. `_meta.comment` of a mapping is stored, and SQL never shows it: REMARKS
is empty for a table and NULL for a column.

Schemas and the current schema are not answered, because there is no schema
(D176). SHOW SCHEMAS gives no row.

### What a second opinion found

Gemini and DeepSeek were asked on 2026-10-08, as hard rule 14 requires.
DeepSeek spent its whole budget on reasoning when the question listed 20 kinds,
and answered a short one. Gemini timed out twice and answered the third, shortest
question. Each lead was run against 9.5.3.

| Lead | Who | Result |
| --- | --- | --- |
| Roles, privileges, settings, comments and constraints are absent | both | Held. No SYS or SHOW statement lists them |
| Indexes from SHOW TABLES, with the kind INDEX | DeepSeek | Wrong. SHOW TABLES lists the indices as tables. The kind INDEX is the index itself and not an index of SQL |
| Partitioned tables from SHOW TABLES, with the kind DATA_STREAM | DeepSeek | Wrong. A data stream has the type VIEW and the kind ALIAS, and its backing index is a TABLE and an INDEX. SQL reports no partition. Not answered |
| Databases from SHOW CATALOGS | the survey | Held, and a close call. It lists the cluster, and a remote cluster beside it |
| Types from SYS TYPES | the survey | Held |
| The current user from USER() | the survey | Held |
| Roles and privileges from a read of `.security-7` | the survey | A stretch. It needs a document read of a restricted index, only native users are in it, and no role name is. Not answered |
| Routine parameters from SHOW FUNCTIONS | the survey | A stretch. It has no synopsis. Not answered |

### What the fixture builds

`models/elasticsearch/fixture` is a list of HTTP requests, because SQL cannot
make anything. It makes `dbmeta_author`, `dbmeta_book`, `dbmeta_region` and
`dbmeta_shipment`, which are the four core tables of D53, the alias
`dbmeta_recent` over `dbmeta_book`, `dbmeta_types` with one field of each unusual
type, the data stream `dbmeta_stream`, and `secret_idx` for the parity test.
Every name but the last starts with `dbmeta`, because the role of the ordinary
user reads `dbmeta*`. A key, a foreign key, NOT NULL, a default and a field
comment cannot be built, and `TestElasticsearchLeavesOut` asserts that the kinds
that need them are not answered.

### What the conformance test says

Elasticsearch is out of the main agreement count. An index has no key, no
constraint and no NOT NULL, and the report removes the prefix `dbmeta_`. SYS
COLUMNS sorts the fields of a mapping by name, so the ordinal is the position in
that order, and `name.raw` and `dims.h` are columns.

### Which answers depend on who is asking

Elasticsearch shows a user the indices that the role of the user can read. The
ordinary user, who can read `dbmeta*`, sees every index of the fixture and not
`secret_idx`, so tables and columns return fewer rows than the administrator.
A reader who can read `dbmeta_author` alone sees that index, so tables, columns
and views return fewer rows. No query is refused to either principal. The
current user differs, as it does everywhere.

`GET /` needs the cluster privilege `cluster:monitor/main`, and the role of the
ordinary user holds it, so `SELECT version()` gives the ordinary user the same
release as the administrator. `TestElasticsearchVersionForAnOrdinaryUser`
asserts it (D192). The sections
`elasticsearch/same/user` and `elasticsearch/same/reader` hold the rest.

8.19.22, 9.4.6 and 9.5.3 answer the same for all eight kinds and for both
principals.
## Apache Solr

`models/solr` answers 4 of the 65 on Apache Solr 9.9.0, 9.10.1 and 10.0.0,
measured on 2026-10-08 through dbimp's solr driver, which is what dburl's solr
scheme opens. 9.9.0 and 10.0.0 are Tested and 9.10.1 is Nightly. Solr answers
SQL with Apache Calcite, at `POST /solr/{collection}/sql`, and the three
releases answer every statement the same way. D179 holds the mapping and the
reasons for it, for Ken to review.

### What it reads

Solr SQL has one catalog, the schema `metadata`, with two tables: TABLES and
COLUMNS. They are in the form of JDBC's DatabaseMetaData. There is no
INFORMATION_SCHEMA, no SCHEMAS and no FUNCTIONS table, and SHOW is a syntax
error. Every query is one statement over one of the two, and no kind is a
walk.

Solr has no DDL in SQL. A collection, a field and an alias come from the
Collections API and the Schema API.

### What it answers

Schemas, the current schema, tables and columns.

A collection is a table, and an alias is a table too, because SQL lists it as
TABLE and cannot tell it from a collection. Every collection is in the schema
`solr`. Solr itself names the schema with the address of ZooKeeper, which
changes with the machine, so the model reports the fixed name `solr` (D176).
The catalog is `solr` for the same reason. The two tables of `metadata` are
tables of the type `system table` in the schema `metadata`, and they are listed
only with the system objects.

A column is a field. Solr adds `_nest_path_`, `_root_`, `_text_`, `_version_`,
`_query_` and `score` to the fields of the schema. Every column reads
nullable, with no default and no key, including the unique key `id`, because
the SQL module reports nothing else. The type is VARCHAR, BIGINT, DOUBLE,
TIMESTAMP or ANY. A boolean field reads VARCHAR and a multi-valued field reads
ANY, and the type of the field in the schema is not there. The ordinal is the
position that COLUMNS gives: the fixed fields first, then the fields of the
collection by name, then `_query_` and `score`.

The version query is `SELECT version()`. The SQL of Solr has no such function,
and dbimp's driver answers it from `GET /solr/admin/info/system`, which holds
`lucene.solr-spec-version`. The ordinary user can read it, because security.json
holds a rule for that path. The same user is still refused the rest of the admin
API (D191, D192).

### What it cannot answer, and why

52 kinds. Most are absent from Solr SQL.

`Views` is not answered. An alias is the nearest thing, and `LISTALIASES` of
the Collections API is the only source that tells it from a collection. It is
HTTP and not a statement. `PartitionedTables` is the same: `CLUSTERSTATUS`
holds the shards. `Roles`, `RoleGrants` and `Privileges` are in security.json,
which is an HTTP read for an administrator. `Settings` are the configuration
of each collection, also HTTP. `Databases` is not answered, because Solr names
no cluster in SQL. `Functions` is not answered, because Calcite lists none in
a table. `CurrentUser` is not answered: `CURRENT_USER` and `USER` return `sa`
for every user, which is a constant of Calcite and not the user.

### What a second opinion found

Gemini and DeepSeek were asked on 2026-10-08, as hard rule 14 requires. The
first long questions ran out of tokens for both. Gemini answered a short one
on the third try, and DeepSeek answered a short one with a budget of 8000
tokens. Each lead was run against 10.0.0.

| Lead | Who | Result |
| --- | --- | --- |
| Every other kind from a table in `metadata` | DeepSeek | Absent. FUNCTIONS, SCHEMAS and information_schema tables all fail as unknown objects |
| The current user from `CURRENT_USER`, `USER` and `SESSION_USER` | Gemini | Wrong. They return `sa` for the administrator and for the ordinary user, a constant of Calcite |
| The current schema from `CURRENT_SCHEMA` | Gemini | Wrong. Solr answers Unable to implement. The model reads the fixed schema instead |
| The current catalog from `CURRENT_CATALOG` | Gemini | Held, and empty. It names no catalog |
| Indexes from `GET /solr/{collection}/schema/fields` | Gemini | A stretch. It is HTTP, only an administrator can read it, and a field flag is not an index. Not answered |
| The release from a statement | both | Absent. `version()` does not exist. Only the system handler has it |

Both models called every other kind absent, and the survey agrees. The Luke
handler and the StatsComponent work for the ordinary user and need one request
for each collection or field, which is not a statement. Neither is answered.

### What the fixture builds

`models/solr/fixture` makes the four core collections, author, book, region
and shipment, and the alias `recent` of book. A test sends the requests over
HTTP as the administrator, because the driver reads and never writes. Each
collection has a configuration set of its own, because collections made from
`_default` share one managed schema. It cannot build a foreign key, a default,
an index, a trigger, a sequence, a comment or a type, and `TestSolrLeavesOut`
asserts that those kinds are not answered.

### What the conformance test says

Solr is out of the main agreement count. A collection has no key, no
constraint and no view in SQL, so the section holds the four collections, the
alias, and their columns, all nullable, with no key.

### What it costs

metadata.TABLES and metadata.COLUMNS read every collection of the cluster, and
a filter does not prune. The cost is linear and about 1 ms for each
collection while the heap is not under pressure. Each collection takes 3 to
4 MB of heap, so a node with the heap of the entry, 1 GB, died at about 300
collections, and one with 3 GB died between 800 and 1600. At 1062 collections
on 3 GB, TABLES took 70 ms and COLUMNS 0.95 s. Under memory pressure they
took 9 to 31 s, and COLUMNS left 114 of 1065 collections out with no error.
D179 holds the measurement. A consumer that reads a large cluster must read
TABLES, which is cheap, and must not rely on COLUMNS to name every collection.

### Which answers depend on who is asking

Nothing in the four queries. The ordinary user, who has the role search, sees
the same rows as the administrator in all four, on all three releases. The
administrator alone can read the release, and the ordinary user gets HTTP 403.
A reader with less than the role search was not measured.

## OpenSearch

`models/opensearch` answers 3 of the 65 on OpenSearch 3.9.0, and 2 of the 3 on
2.19.6, measured on 2026-10-08 through dbimp's opensearch driver, which is what
dburl's opensearch scheme opens. Both releases are Tested. OpenSearch answers
SQL on `POST /_plugins/_sql`. D181 holds the mapping and the reasons for it, for
Ken to review.

### What it reads

Its SQL has no table that a SELECT reads. It has SHOW TABLES LIKE and DESCRIBE
TABLES LIKE, and both need a pattern. So each kind is a walk (D175). One SHOW
TABLES LIKE % lists every index, and one DESCRIBE TABLES LIKE for each index
lists its fields. A pattern in DESCRIBE merges every index that matches into one
table named for the pattern, and an underscore in SHOW TABLES LIKE is a wildcard
with no escape. So the walk matches the name in Go and gives DESCRIBE the exact
name of one index.

400 indices were listed in 16 milliseconds and described in 1.7 seconds in the
survey, and the cost check of D181 read 200 indices in 181 milliseconds on 3.9.0.
The server reads the mapping in the cluster state and never a document.

### What it answers

Databases, tables and columns.

The cluster is the catalog and the one database. There is no schema, so every
schema is empty. An index is a table. A field is a column, and an object and a
nested field are columns too, with the types object and nested. A subfield is a
column of its own, such as `dims.h`. DESCRIBE leaves out a multi-field, such as
`name.raw`, and a field of a type SQL cannot read, such as integer_range. The
type is the name of the mapping type in lower case, and a date is `timestamp`.
Every column is nullable and none has a key, a default or a comment. The
position starts at 0.

An alias is a table on 2.19.6, because SHOW TABLES lists it as BASE TABLE, the
same as an index. 3.9.0 does not list an alias. So Views is not answered on
either release.

The version query is `SELECT version()`. The SQL of the server fails that
statement, and dbimp's driver answers it from `GET /`, which holds
`version.number`. Every user can read it. On 2.19.6 the role of the ordinary user
holds the permission `cluster:monitor/main`. On 3.9.0 every user gets it from the
header `X-OpenSearch-Version` (D191, D192).

### Columns on 2.19.6

dbimp v0.14.0 did not read a row of DESCRIBE TABLES on 2.19.6, because that
release declares every column of the answer keyword and sends numbers in some
of them. dbimp v0.14.1 reads the number as the value that arrived. So Columns
answers on both releases, with the position counting from 0 (D189).

### What it cannot answer, and why

53 kinds. Most are absent from OpenSearch SQL. Roles, role grants and privileges
are in the security plugin, which is HTTP. Settings are in the settings APIs.
The `_meta.comment` of a mapping is stored, and SQL never shows it. SHOW
SCHEMAS, CATALOGS, FUNCTIONS, COLUMNS, DATABASES, GRANTS and VARIABLES fail with
HTTP 400, and so do `SELECT VERSION()`, `SELECT USER()` and `SELECT DATABASE()`.
There is no information_schema.

### What a second opinion found

Gemini and DeepSeek were asked on 2026-10-08, as hard rule 14 requires. Both ran
out of tokens on long questions, because DeepSeek spent its whole budget on
reasoning, and Gemini timed out on three of five short ones. Each lead was run
against 3.9.0.

| Lead | Who | Result |
| --- | --- | --- |
| Views, roles, settings, comments and the current user are absent | both | Held |
| Indexes from SHOW TABLES LIKE | both | Wrong. SHOW TABLES lists the indices as tables. It lists no index of SQL |
| Function list from SHOW FUNCTIONS LIKE | DeepSeek | Wrong. The statement fails with HTTP 400 and the message that only SHOW TABLES LIKE exists. DeepSeek said that the premise of the question was incomplete, and it was not |
| Data types from DESCRIBE | DeepSeek | A stretch. The distinct TYPE_NAME of the columns of a cluster is a list of types in use and not a list of types. Not answered |
| Privileges, partitioned tables and the rest | DeepSeek | Held as absent |
| Roles and privileges from the security plugin | the survey | HTTP and not SQL. Not answered |
| Current user from `/_plugins/_security/authinfo` | the survey | HTTP and not SQL. Not answered |
| Databases from TABLE_CAT of SHOW TABLES | the survey | Held, and a close call. It names the cluster only when the user sees an index |

### What the fixture builds

`models/opensearch/fixture` is a list of HTTP requests, because SQL cannot make
anything. It makes `dbmeta_author`, `dbmeta_book`, `dbmeta_region` and
`dbmeta_shipment`, which are the four core tables of D53, the alias
`dbmeta_recent` over `dbmeta_book`, `dbmeta_types` with one field of each unusual
type, `dbmeta_empty` with no field, and `secret_idx` for the parity test. Every
name but the last starts with `dbmeta`, because the role of the ordinary user
reads `dbmeta*`. A key, a foreign key, NOT NULL, a default and a field comment
cannot be built, and `TestOpenSearchLeavesOut` asserts that the kinds that need
them are not answered.

### What the conformance test says

OpenSearch is out of the main agreement count. An index has no key, no
constraint and no NOT NULL, the report removes the prefix `dbmeta_`, and DESCRIBE
lists an object and a nested field as columns. 2.19.6 has a section of its own,
`opensearch@2`, because it lists the alias `recent` as a table.

### Which answers depend on who is asking

The role of the ordinary user holds `indices:admin/get` on every index, so the
user sees the name of every index, `secret_idx` included. The plugin refuses
DESCRIBE of an index that the role does not read, so Columns returns no row for
`secret_idx`, and the walk goes on. Tables and Databases answer the same rows
as for the administrator. A reader who can read `dbmeta_author` alone is refused
SHOW TABLES, because the plugin needs `indices:admin/get` on every index to run
it, so Tables, Databases and Columns are refused to the reader. A lister who has
`indices:admin/get` on every index and nothing else gets every table and no
column. The sections `opensearch/same/user`, `opensearch/same/reader` and
`opensearch/same/lister` hold the rest. The sections of `opensearch@2` record the same
differences on 2.19.6.

`GET /` needs the cluster permission `cluster:monitor/main`, and the role of the
ordinary user has none. So on 2.19.6 `SELECT version()` is refused to the
ordinary user with HTTP 403 and `security_exception`. 3.9.0 sends the header
`X-OpenSearch-Version` with every answer, and 2.19.6 sends none, so on 3.9.0 the
ordinary user reads the release. `TestOpenSearchVersionForAnOrdinaryUser`
asserts both.

### How the two releases differ

The two answered the fixture the same way for SHOW TABLES and DESCRIBE, with
three differences. 2.19.6 lists an alias in SHOW TABLES and 3.9.0 does not. 2.19.6
declares the columns of DESCRIBE as keyword and 3.9.0 declares integers, and
dbimp v0.14.1 reads both. The survey also said that the position starts at 1
on 3.9.0, that 3.9.0 lists every mapping type, and that DESCRIBE of an index with
no mapping fails with HTTP 500 on 3.9.0. None of that held when it was measured
again, and the position starts at 0 on both.

## GizmoSQL

`models/gizmosql` answers 21 of the 65 on GizmoSQL 1.40.0 and 1.41.0, measured
on 2026-10-08 through the Arrow Flight SQL driver in `arrow-go`, which dburl
names for the scheme gizmosql. Both releases run DuckDB 1.5.6 and are Tested.
GizmoSQL is a server for Arrow Flight SQL, and its engine is DuckDB, or SQLite
when it starts with `--backend sqlite`. The entry starts DuckDB, and this model
is for that backend only. D187 holds the decisions.

### What it reads

The DuckDB catalog, through the same statements as [DuckDB](#duckdb). The model
shares all 20 bindings of `models/duckdb` and writes none. How much is shared:
every statement, and the fixture. The answers on the fixture are line for line
DuckDB's, and the conformance section of `gizmosql` is the same as the one of
`duckdb`.

Two things differ from the library, and each is a fragment of the duckdb
model.

- GizmoSQL attaches a database named `_gizmosql_system`, which holds two views
  for the Flight SQL metadata calls. DuckDB does not flag it internal, so a
  fragment that gates on the version key `gizmosql` leaves it out unless the
  caller asks for the system objects. `TestGizmoSQLSystemDatabase` checks it.
- The `types` filter of Tables casts its parameter to VARCHAR. Without the
  cast, DuckDB cannot type the parameter when it prepares the statement,
  GizmoSQL reports every parameter as a string, and the driver refuses the
  boolean one.

### The driver, which opens no session

The driver sends the user and the password on each call and never makes the
handshake that GizmoSQL needs, so every statement fails with "No session ID in
request context". usql's gizmosql driver is in its bad group for the same
reason. The tests make the handshake themselves, with the Flight client of the
same module, and hand the driver the token (`test/internal/gizmosql`). The
fault is in `arrow-go`.

### What the driver returns

Every column the model selects arrives as a typed Go value. The driver names no
database type: `ColumnTypeDatabaseTypeName` is empty for every column. It
refuses an unsigned 64 bit integer, a date, a list, a map, a struct, an enum
and an interval, and reads `HUGEINT` and `DECIMAL` as a `float64`. No statement
of the model returns one of those. A statement that ends in a semicolon runs,
so the model does not strip it. A query of 3,000 tables, 1,000 views and
1,000 indexes takes at most 25 ms for any kind, against a catalog of 4,005
tables and 17,016 columns, measured with the fixture also in place.

### What it cannot answer

The 36 kinds that DuckDB cannot answer, for the same reasons: no role, grant,
trigger, partition, foreign object, replication, text search or operator
catalog. The core has one user, and GizmoSQL adds no SQL catalog of its own in
the core. `gizmosql_metrics()` needs an enterprise license.

### What a second opinion found

Gemini and DeepSeek were asked. Gemini named `duckdb_indexes()` for index
columns, which DuckDB rejected already because the columns are text in a list,
`summarize` and `PRAGMA storage_info` for column statistics, which scan the
data (D162), `duckdb_extensions()` for extension objects, which is the
extensions kind and not the objects of an extension, and `duckdb_types()` for
domains, which DuckDB does not have. It said the core adds no catalog. That
held. DeepSeek named `gizmosql.sessions` and `gizmosql.users`,
`pg_catalog.pg_roles`, `pg_user` and `pg_cast`, and `information_schema`
views for roles, privileges, triggers and domains. None of the nine exists on
1.40.0 and the server says so for each. Every lead of DeepSeek was invented.

### Flight SQL metadata calls

`GetCatalogs`, `GetDbSchemas`, `GetTables`, `GetPrimaryKeys`, `GetSqlInfo`
and the others are calls of the protocol, and SQL does not reach them. The
model does not use them. `GetSqlInfo` names the server `gizmosql` and the
engine `duckdb v1.5.6`, and does not give the GizmoSQL release.

### Which answers depend on who is asking

Not measured. The core has one user, `admin`, and the other roles need an
identity provider or an enterprise license, so the model has no parity target
(`parityExempt`, D187).

### The describe fields (D208)

Every statement is the duckdb model's, so GizmoSQL fills the same fields as
DuckDB, and answers the NotNulls kind. The tests run the same checks on it.

## Spanner

`models/spanner` answers 22 of the 65, in the GoogleSQL dialect, measured on
2026-10-10 on Spanner Omni 2026.r4-lts and on Cloud Spanner. Omni runs with
go-sql-spanner, which usql uses and which reaches gRPC. Cloud Spanner runs with
`github.com/xo/dbimp/spanner`, which dburl v0.51.0 names, since D231. The two
drivers register one name, so each has its own test binary. Spanner Omni is the engine
of Cloud Spanner as a container. The two answer the same 47 views of
INFORMATION_SCHEMA. See D216 and D219.

A database in the PostgreSQL dialect has the catalog of PostgreSQL. The model
does not read it, and `models/postgres` does.

### Which catalog it reads, and why

INFORMATION_SCHEMA, and nothing else. Spanner Omni lists 47 views in it, and the
model reads 30. The statistics in SPANNER_SYS are measurements of load and not
a catalog, so no kind reads them, except the one number that the version uses.

The default schema is named by the empty string. A schema made with CREATE
SCHEMA has a name, and INFORMATION_SCHEMA and SPANNER_SYS are the two Spanner
keeps for itself. A query hides those two unless the caller sets `with_system`.
An empty schema pattern means every schema, so no pattern selects the default
schema alone. The fixture builds its objects in the schema dbmeta_fixture and
one table in the default schema, so that a test reads both.

### What it answers

Schemas, tables, columns, indexes, index columns, constraints, constraint
columns, not null constraints, views, sequences, functions, routine parameters,
roles, role grants, privileges, column privileges, settings, the current schema,
the current user, publications, publication tables and tablespaces.

A table is a base table, a view or a synonym. A synonym has rows of its own
in the views, so old_ledger lists a primary key and its NOT NULL constraints as
the table it stands for does. Its options say what Spanner
records about it beyond the columns: the parent table, how it is interleaved and
what happens to a child when its parent row goes, the row deletion policy, the
locality group, and the table a synonym stands for. An interleaved table is not
inheritance, because the child has no column of its parent, so `Inherits` is
not answered. A row deletion policy deletes old rows and restricts no access, so
`Policies` is not answered.

An index is a row of INDEXES. The primary key is an index named PRIMARY_KEY. A
foreign key makes an index that Spanner names IDX_ and marks managed, and the
model says so in the options. A null filtered index and a vector index keep a
FILTER, which is the predicate. A search index reports unique, which is what
Spanner says. A key column has an ordinal and a column the index STORES has none,
so the model numbers the stored ones after the keys in the order of their names.

Constraints are the primary key, the foreign key and the check constraint.
Spanner has no unique constraint and a unique index stands in for it. Spanner
keeps each NOT NULL column as a check constraint named CK_IS_NOT_NULL_, and
Spanner refuses that prefix in a name that a person writes. `Constraints` leaves
those out, as psql 18 does, and `NotNulls` reads them. A foreign key points at the
primary key of a table or at a unique index, and the model reads the column it
points at from the key columns in the first case and from the index columns in
the second.

A database role is a name that grants attach to. It cannot log in, has no flag
and has no password, so every field of `Role` but its name and its membership is
a fixed answer, and each is described as one. A role grant is a row of
ROLE_GRANTEES. Privileges are the grants on a table, a view, a change stream and
a function, folded into one text for each object, and the grants that name a
column are in `ColumnPrivilege` and in the column access of the object. COLUMN_PRIVILEGES
also holds every column of a table that was granted as a whole, and the model
leaves those rows out, because the grant is not on the column.

### What a second opinion found

Gemini and DeepSeek sorted the unanswered kinds. Every lead was run on the server.

- Gemini named the change streams as publications, and that is right. The view
  CHANGE_STREAMS, with its tables, columns and options, answers `Publications`
  and `PublicationTables`.
- Gemini named table interleaving as inheritance. It is not one, as above.
- Gemini named ROW_DELETION_POLICIES as policies. It is not one, as above.
- Gemini named SESSION_USER() for the current user. Cloud Spanner answers it with
  the IAM principal, so `CurrentUser` reads it. Spanner Omni with no authentication
  refuses it with "Cannot compute SESSION_USER, because the user name is unknown",
  so on Omni the query is an error and not an answer.
- DeepSeek named INFORMATION_SCHEMA.COLLATIONS and SPANNER_SYS.COLUMN_STATS. Neither
  exists. SPANNER_SYS.COLUMN_OPERATIONS_STATS counts operations and holds no value
  statistics, and SPANNER_STATISTICS lists the names of the packages of optimizer
  statistics and nothing about a column.
- DeepSeek named TABLE_OPTIONS and COLUMN_OPTIONS as comments. They hold
  locality_group and allow_commit_timestamp, and no comment, because Spanner has
  none.
- DeepSeek named CURRENT_USER(). It does not exist.
- The locality groups as tablespaces came from reading the views and not from a
  model. A locality group names where data is stored, solid state or disk, and that is
  what a tablespace is for. It is an analogue and not an identity.

### What it cannot answer

Absent from Spanner: access methods, languages, conversions, casts, collations,
large objects, event triggers, aggregates, types, domains, operators, role
settings, default privileges, foreign data wrappers, foreign servers, user
mappings, foreign tables, foreign options, subscriptions and the connection of a
subscription, the text search kinds, the operator class kinds, extensions,
extended statistics, comments, triggers, partitioned tables, partitions, rules
and enumerated types. Spanner has a text search index and a vector index, and
neither has a configuration or a dictionary to list. A proto bundle holds the
protocol buffer types of the database, and SCHEMATA keeps it as one binary
descriptor that SQL cannot read.

Present and not answered: `Databases`, because no function returns the name of
the database and DATABASE_OPTIONS holds no name, on Cloud Spanner too. `Inherits`
and `Policies`, as above. `ColumnStats`, as above.

Spanner has objects that no kind names. A model and a property graph have their
own views, MODELS and PROPERTY_GRAPHS. Spanner Omni refuses CREATE MODEL, and the
JSON of a property graph is not a kind. A placement is where a table is
replicated, and it needs an instance partition that a single server does not
have. The backlog holds them.

### The describe fields (D198 to D203)

Fields with no source are absent and say so in their description. Table size is
the one that has a source and is not read: SPANNER_SYS.TABLE_SIZES_STATS_1HOUR
holds a row each hour, and it was empty on a server that was up for minutes. A
sequence has a kind, a skip range and a start, and `Sequence` has one field for
them, the start.

### What the fixture builds

`models/spanner/fixture` builds the schema dbmeta_fixture. It holds author, book,
region and shipment, with a composite key and a composite foreign key, a unique
index, a descending index and a null filtered index with a stored column, the
view recent, a table interleaved in book with an index interleaved in it, a
sequence, a table with a stored generated column, an identity column, a default
that reads a sequence and a hidden TOKENLIST column with a search index, a table
with a row deletion policy and a commit timestamp column, a table in a locality
group with a synonym, a function, and three roles: one with a grant of each
kind, one that belongs to it, and one with none. Three change streams and one
table with a vector index are in the default schema, because Spanner refuses
a vector index or a change stream in a named schema. Cloud Spanner does too.

It builds no placement, no model, no trigger, no rule, no policy and no type,
because Spanner has none of them or Spanner Omni refuses it.

Every step is DDL and takes seconds on Spanner, so the tests run the
steps as one batch and share one fixture.

### What the conformance test says

The section holds author, book, region, shipment and the view recent, with every
column, key and foreign key, and it agrees with the others on every line but one.
Spanner has no unique constraint, so the unique title of book is a unique index
and the line `constraint book unique (title)` is missing, which takes the
relational agreement from 23 lines to 22. That is a fact, so Spanner is in
`agreementExcluded` with the reason. A check constraint has no columns in
KEY_COLUMN_USAGE, so it has no line either, as on SAP HANA.

### Which answers depend on who is asking

A database role changes 16 queries and no query is refused. A session names a role
with the property `database_role`, and Cloud Spanner and Spanner Omni both enforce
it: a role that does not exist is refused, a table that the role holds no grant on
is refused, and INFORMATION_SCHEMA shows the role only what it holds a privilege
on. The role dbmeta_reader, which holds SELECT on a table, a column, a view, a
change stream and a function, reads fewer rows for columns, constraint columns,
constraints, functions, index columns, indexes, not nulls, privileges,
publication tables, publications, role grants, roles, routine parameters,
schemas, sequences and tables. The role dbmeta_stranger holds nothing and reads
fewer rows for those and for column privileges and views. The two servers give the
same answers with one exception: Cloud Spanner leaves the default schema out of
Schemas for the role dbmeta_reader, which holds USAGE on dbmeta_fixture only, and
Spanner Omni lists it. The parity file keeps a section for each, `spanner` and
`spanneromni`.

The service account with databaseReader reads as an IAM principal, gets the
administrator's answer to every query but `CurrentUser`, which reads its own
name, and cannot name a role, because it lacks spanner.databases.useRoleBasedAccess.
It is a principal on Cloud Spanner only.

The first measurement on Spanner Omni said that it enforces no role. It was wrong.
The name of the property is `database_role`, and the test wrote `role`, which
go-sql-spanner ignores without a word (D219). Both drivers name it `database_role`.

### Cloud Spanner and Spanner Omni

Measured on 2026-10-10 with the fixture on both. The views, the optimizer
versions, the schema rules, the hash join hint and the answers of every query agree,
and conformance records one section for both. Parity differs for one query, which
the section above names. Three things differ in the data:

- SESSION_USER answers on Cloud Spanner and fails on Spanner Omni with no
  authentication. `CurrentUser` is an error on Omni.
- The default locality group reports the option storage=ssd on Cloud Spanner and
  no option on Omni, so the tablespace default has options on one and none on the
  other.
- SPANNER_SYS.TABLE_SIZES_STATS_1HOUR holds the sizes that Cloud Spanner measures
  each hour. See the backlog for the size fields.

Both refuse a vector index and a change stream in a named schema, both name the
default schema with the empty string, and both keep the 47 views. Cloud Spanner
logs a warning that the client cannot write its own metrics, because the account
holds no monitoring permission. It changes no answer.

### The cost

Measured on 1600 tables and 3200 indexes in one schema. A subquery that names the
row of the outer statement is run for each row, and Spanner plans a join of two
INFORMATION_SCHEMA views as a nested loop, so the first version of the tables,
the indexes and the NOT NULL constraints took 4.6, 5.7 and 8.4 seconds, which is
twelve times as long for four times the tables. Tables and indexes now read the
options once and join them, and NotNulls hints a hash join. Every statement runs in
under 0.1 seconds on that catalog. Spanner has no window function, so the numbers
of a stored index column and of a column grantee are counted with a join.

## BigQuery

`models/bigquery` answers 18 of the 65, in the GoogleSQL dialect, measured on
2026-10-10 on the hosted service, in the location US, with gorm.io/driver/bigquery
v1.2.1. Schemas, CurrentSchema and Privileges were measured on 2026-10-11, after
the principals gained `roles/bigquery.metadataViewer` on the project. The tests
moved on 2026-10-11 to `github.com/xo/dbimp/bigquery` v0.17.0, which dburl v0.50.0
names. Every test passed on it, and conformance and parity did not change. See D220
and D229.

BigQuery is a hosted service and has no release, so the tier is Verified and CI
never runs it. The local emulator that `dbrun` can start, goccy's
bigquery-emulator, answers SCHEMATA, TABLES, TABLE_OPTIONS and COLUMNS and no
more, so no model reads it and its entry stays Staged. The tests of the model
need the service, and they skip when `DBMETA_BIGQUERY` is not set.

### Which catalog it reads, and why

The views of INFORMATION_SCHEMA that belong to one dataset, and the legacy
metatable `__TABLES__` of the same dataset. BigQuery has a project, which holds
datasets, which hold tables. The model calls the project the catalog and a
dataset a schema. A view of a dataset is named as `dataset.INFORMATION_SCHEMA.TABLES`,
and a query job can carry a default dataset, which the driver sets from the last
part of the URL, `bigquery://project/location/dataset`. The statements name
`INFORMATION_SCHEMA.TABLES` with no dataset, so they read the dataset of the
connection. A statement cannot bind the name of a view, so a model cannot read a
second dataset, and the `schema` pattern can only narrow the one it reads. A
caller that wants another dataset opens another connection.

The model reads 21 views: SCHEMATA, SCHEMATA_OPTIONS, OBJECT_PRIVILEGES, TABLES, COLUMNS, COLUMN_FIELD_PATHS, TABLE_OPTIONS,
TABLE_CONSTRAINTS, KEY_COLUMN_USAGE, CONSTRAINT_COLUMN_USAGE, VIEWS, ROUTINES,
PARAMETERS, ROUTINE_OPTIONS, PARTITIONS, SEARCH_INDEXES, SEARCH_INDEX_COLUMNS,
SEARCH_INDEX_OPTIONS, VECTOR_INDEXES, VECTOR_INDEX_COLUMNS and
VECTOR_INDEX_OPTIONS. The descriptions of tables, columns and routines are in
TABLE_OPTIONS, COLUMN_FIELD_PATHS and ROUTINE_OPTIONS, and no view holds a
comment otherwise. The size and the row count of a table are in `__TABLES__`.
TABLE_STORAGE has them too, but it is a view of the region and needs a
permission on the project.

### What it answers

Tables, columns, indexes, index columns, constraints, constraint columns, views,
functions, aggregates, routine parameters, comments, partitioned tables,
partitions, the current user, databases, schemas, the current schema and
privileges. The last three need `roles/bigquery.metadataViewer` on the project
(D226).

A table is a base table, a view, a materialized view, an external table, a clone
or a snapshot, and `Table.Type` spells them table, view, materialized view,
external table, clone and snapshot. The comment of a table is its description
option. BigQuery holds an option as the text of a GoogleSQL literal, such as
`"people who write books"` with the quotes and the escapes, and the model reads
the literal back to the text it stands for. The other options are in
`Table.Options` with their literals as BigQuery writes them, and so is the
clustering of the table, as `cluster_by`, and the base table of a clone or a
snapshot, as `base_table`. The size and the rows are exact and come from
`__TABLES__`, and a view and an external table have none.

COLUMNS reports the text NULL for a column with no default and for a column with
no collation. A column can have a default, and a default can be the expression
NULL, which means the same as none, so the model turns the text into an absent
value. The other columns of the view are NULL where they mean it. An identity
column is `always` or `by default`. BigQuery refused every generated column that
was tried, with "Unsupported generated column expression", so `generated` is
mapped from IS_GENERATED and is not measured.

BigQuery has two kinds of index, a search index and a vector index, and no other.
The primary key is a constraint and no index backs it. An index has a status,
and an index on a table that is below the size threshold is TEMPORARILY
DISABLED, so `Index.Valid` is false for both indexes of the fixture. The model
reads the DDL of the index as the definition.

BigQuery has a primary key and a foreign key, and it never enforces either, so
`Constraint.Enforced` is false for each. It has no unique constraint and no check
constraint. A constraint name begins with the table and a dot, as
`book.book_author_fk`, and a primary key is named `table.pk$`. The model reports
the name as BigQuery gives it. A foreign key points at the primary key of a table,
CONSTRAINT_COLUMN_USAGE names the table on the rows of the key, and
KEY_COLUMN_USAGE gives the position of each column in the primary key of that
table, so the model reads the column from the key columns of that table. A key
that points at a table in another dataset has no readable primary key, so its
column is absent and its table is not.

A routine is a function, a table function, a procedure or an aggregate function.
`Functions` answers the first three and `Aggregates` the last, because
CREATE AGGREGATE FUNCTION makes a user defined aggregate. The definition is the
DDL of the routine and the source is its body. A function in JavaScript has a
language of javascript, and one declared DETERMINISTIC is immutable. A procedure
has IN, OUT and INOUT parameters. A function has no mode, so its parameters are
in and its returned value is a row of ordinal zero with the mode return.

### What the models call analogues

- A search index and a vector index are indexes, because psql lists an index of
  any method with \di.
- A user defined aggregate function is an aggregate.
- A partitioned table is a table that has PARTITION BY in its DDL. INFORMATION_SCHEMA
  has no column for the clause, so the expression is the text after the words.
  Every BigQuery partition covers a range of dates, of times or of an integer, so
  the strategy is range. A partition has an id, such as 20260101, and a table
  decorator names it, as `sales$20260101`, so a partition is its table, a dollar
  sign and its id. A table that is not partitioned has one row in PARTITIONS with
  no id, and the model leaves it out.
- A dataset is a schema, and the connection reads one of them. SCHEMATA lists
  every dataset of the project and `@@dataset_id` is NULL, so the statement keeps
  the dataset that the tables of the connection name. A dataset with no table or
  view has no row. The description is the comment, and the other options are in
  `Schema.Options`, such as `location="us"` and `default_table_expiration_days=7.0`.
  The owner is empty, because access is by IAM (D226).
- The IAM bindings of the dataset are its privileges. OBJECT_PRIVILEGES answers one
  object, and a table has no binding of its own unless somebody sets one, so a
  table has no row. One row is one binding, as `serviceAccount:name@project.iam.gserviceaccount.com=roles/bigquery.dataOwner`,
  with no grantor. The statement builds the name of the regional view as text,
  from the location in SCHEMATA, and runs it with EXECUTE IMMEDIATE (D226).
- A project is a database. BigQuery lists no other project than the one a session
  runs in, so `Databases` reports that one, which is what `@@project_id` holds.
  The statement reads a system variable, and the root package now leaves
  `@@name` in a statement as it is (D220).

An external table is not a foreign table. It has no server, and a person names the
files it reads in `uris` and the format in `format`, which are options of the
table, so `Table.Options` holds them and `ForeignTables` is not answered. Ken can
reverse that.

### What a second opinion found

Gemini and DeepSeek sorted the unanswered kinds. Both failed on the long question.
Gemini timed out twice and, on a short question, answered four of seven items before
its answer stopped. DeepSeek returned nothing on four requests, because its reasoning
used every token, and answered a short question with a large token limit on the
fifth. Every lead was run on the service.

- Gemini and DeepSeek named INFORMATION_SCHEMA.OBJECT_PRIVILEGES for the grants on a
  table or a dataset, and Gemini said a dataset principal can read it with a dataset
  prefix. The dataset form answers 403, which is what the service says for a view that
  does not exist. The region form answers a query that has one `object_name =` term at
  the root, and the name is a dataset unless an `object_schema` term names one. A
  query with IN or with OR is refused. So one statement cannot list the grants of every
  table. It does answer for the dataset, with the IAM roles and their grantees, and
  `Privileges` reads that (D226). A table of the fixture has no row of its own.
- Gemini named ROW_ACCESS_POLICIES with a dataset prefix, and DeepSeek named it in the
  region. DeepSeek also named DATA_POLICIES, CONNECTIONS and LINKED_DATASETS. With the
  project role, each answers not found in the region and in the dataset, as a made up
  name does. They answered 403 before the role, so that refusal did not prove an
  absence, and this answer does (D226).
- Gemini named the policy tags of a column in COLUMNS and COLUMN_FIELD_PATHS as
  data masking. The columns hold a structure that the driver returns as a Go value
  and not as text. The fixture has no taxonomy of Data Catalog to attach a tag
  from, so nothing was measured, and `Policies` is not answered.
- DeepSeek named SCHEMATA for the datasets. It exists, and `Schemas` reads it with
  SCHEMATA_OPTIONS now that the principals hold the project role (D226).

### What it cannot answer

Absent from BigQuery: sequences, triggers, user defined types, enumerated types,
domains, extensions, rules, inheritance, access methods, tablespaces, conversions,
casts, languages, large objects, event triggers, operators, operator classes, the
text search kinds, foreign data wrappers, foreign servers, user mappings,
publications, subscriptions, extended statistics and not null constraints. A NOT
NULL is the mode REQUIRED of a column and has no name, so it is in `Column.Nullable`.
BigQuery has no role and no user in SQL, because access is by IAM.

Present and not answered:

- `ColumnPrivileges`, `Roles` and `RoleGrants`. The grants of BigQuery are IAM
  bindings, and the one view that shows them takes one object at a time.
- `Policies`. The views ROW_ACCESS_POLICIES, DATA_POLICIES, CONNECTIONS and
  LINKED_DATASETS answer not found in the region and in the dataset, since the
  principals hold the project role, so they are not views of this service.

- `Settings`. The options of a dataset are in SCHEMATA_OPTIONS, and no view
  holds the settings of the service.
- `ColumnStats`, `Collations` and `Types`. BigQuery keeps no statistics of a
  column that a view reads, a column has a collation name and no list of collations
  exists, and a type is a built in name.

`Index.Using` is the index_type of a vector index, and it is absent for a search
index, which has an analyzer in its options and no method.

### The describe fields (D198 to D203)

Fields with no source are absent and say so in their description. Table size and
rows have one, `__TABLES__`. The comments of tables, columns and routines have
one. Owner, access method, persistence of a view, row security, storage,
compression, statistics target and the rest of the PostgreSQL fields have none.

### What the fixture builds

`models/bigquery/fixture` builds its objects in the dataset dbmeta, because the
test account cannot create a dataset. It holds author, book, region and shipment,
with a composite key and a composite foreign key that BigQuery does not enforce,
the view recent, a table partitioned by a date and clustered by a column with
a search index and two rows, a materialized view over it, a table with an identity
column, a table with an array of numbers and a vector index, a snapshot and a
clone of author, an external table over a public file, a row access policy, a
function with a description, a function in JavaScript, a table function, an
aggregate function and a procedure. It drops everything it made, and each table it
makes expires in seven days anyway.

It builds no sequence, trigger, type or role, because BigQuery has none. It builds
no generated column, because BigQuery refused every one. The row access policy is
there so that a test can show that `Table.RowSecurity` stays absent, because no view
lists the policy.

BigQuery refuses a vector index on a table whose vectors are all NULL, so the table
has 6000 rows. The index is TEMPORARILY DISABLED until the table is larger than
10 MB, as the search index is until its table is larger than 10 GB. Both exist and
are listed.

BigQuery limits the DDL statements that make or drop a search index or a vector
index on one table in a day, and it counts by the name of the table. After many
runs in one day, CREATE SEARCH INDEX or CREATE VECTOR INDEX fails with
quotaExceeded. The teardown does not drop the two indexes, because dropping the
table drops them, and a test that meets the limit skips that index and says so.

### What the conformance test says

The section holds author, book, region, shipment and the view recent. BigQuery has
no unique constraint and no check constraint, so the lines for the unique title and
the check of book are missing. The key columns of the core tables have no default,
because they are plain columns, and the columns of a view are nullable, where
MySQL says they are not. So BigQuery is in `agreementExcluded` with that reason.
Every other line agrees.

### Which answers depend on who is asking

The reader, which holds dataViewer on the dataset and jobUser on the project, gets
the administrator's answer to every query but `CurrentUser`, which reads its own
name. No query is refused and none reads fewer rows, because dataViewer lists the
metadata of every table in the dataset and `__TABLES__` as well. Schemas,
CurrentSchema and Privileges give the reader the same rows too, because dbsetup
gave both principals `roles/bigquery.metadataViewer` on the project (D226). The
rows of Privileges name the IAM principals of the dataset, and the parity file
records no row.

### The cost

BigQuery bills the bytes a query reads, and a query of INFORMATION_SCHEMA is billed
at least 10 MB for each view it reads. A dry run of the fifteen statements on the
fixture reports 10 MB for constraints and partitions, 20 MB for index columns,
partitioned tables and routine parameters, 30 MB for tables, views, functions,
aggregates and constraint columns, 40 MB for columns, indexes and comments, and
nothing for databases and the current user. A whole pass is about 370 MB. A run
of the tests costs several passes, and the free tier is 1 TB of queries a
month. The three kinds of D226 add TABLES, SCHEMATA, SCHEMATA_OPTIONS and
OBJECT_PRIVILEGES, at 10 MB or more each, and a pass is then about 450 MB. The statements read each view once and join the aggregate of it, so the
cost grows with the number of views and not with the number of tables, and no
statement names an outer row in a subquery. A catalog with thousands of tables
was not built, because a table is a job and takes seconds.

## Amazon Athena

`models/athena` answers 8 of the 65, in the Trino based dialect of Athena engine
version 3, measured on 2026-10-10 on the hosted service in us-east-1, with
`github.com/uber/athenadriver` v1.1.15. The tests moved on 2026-10-11 to
`github.com/xo/dbimp/athena` v0.17.0, which dburl v0.50.0 names, and the model lost
`Info.Literal`. Conformance and parity did not change. See D222 and D229.

Athena is a hosted service, so the tier is Verified and CI never runs it. The tests
need the service and skip when `DBMETA_ATHENA` is not set. Each statement is a job
that takes one to six seconds, and a run of the tests takes about five minutes.

### Which catalog it reads, and why

The AWS Glue Data Catalog holds the databases, tables and columns, and Athena shows
them through the INFORMATION_SCHEMA of the catalog `awsdatacatalog`: schemata,
tables, columns and views. The model calls the catalog the database, a Glue database a
schema, and a Glue table a table. The session reads one catalog, and Athena offers no
way to list another, because `SHOW CATALOGS` is a syntax error.

Every other source was measured and refused:

- `system.metadata`, `system.jdbc` and `system.runtime` answer "Queries of this type
  are not supported". The Trino model reads them, so it cannot be copied here.
- `information_schema.roles`, `enabled_roles`, `applicable_roles` and
  `table_privileges` answer NOT_SUPPORTED.
- `SHOW SESSION` and `SHOW STATS` are syntax errors, and `SELECT version()` is
  FUNCTION_NOT_FOUND.
- `SHOW FUNCTIONS`, `SHOW CREATE TABLE`, `SHOW TBLPROPERTIES`, `SHOW PARTITIONS`,
  `SHOW COLUMNS` and `DESCRIBE` answer, and they are statements that a SELECT cannot
  filter, join or alias. They are what holds the comment of a table, the location, the
  serde, the table properties and the function list.
- The hidden tables `"t$partitions"`, `"t$properties"`, `"t$files"` and `"t$history"` are
  per table. `$properties` of a Hive table answers "Relation contains no accessible
  columns", and it is for Iceberg.

### What it answers

Databases, schemas, tables, columns, views, partitioned tables, the current schema and
the current user.

A Hive table, an external table, a table that CTAS makes and an Iceberg table are all
`BASE TABLE`, so `Table.Type` is table for each and view for a view. TABLES has four
columns and none is a comment, an owner, a size or a row count, so those fields are
absent. IS_NULLABLE is YES for every column, because a Glue column has no NOT NULL.

The comment of a column is in COLUMNS. A column that Athena made without a comment has
a NULL, and a column that Redshift Spectrum made has the empty text, which Glue holds.
The model reports both as they are.

A partitioned table is a Hive table with a partition column, and COLUMNS marks the
column with the extra_info `partition key`. The partition columns come after the others,
and a table with two has two rows, as the Hive model gives it. An Iceberg table
partitioned by a transform, such as `day(created)`, has no mark, so it is not listed.

`current_user` is the number of the AWS account and not the name of the IAM user, so
two users of one account read the same value.

### The driver, and the literals

Three faults of the driver of Uber shaped the first tests and the model (D222).
The driver of dbimp has none of them (D229).

- The driver of Uber accepted only the scheme `s3`, named the workgroup with the key
  `workgroupName`, and stopped a query with "Missing data at column" on a NULL unless
  the connection set `missingAsNil=true`. The driver of dbimp reads the URL of
  dburl as it is, and returns a NULL as nil.
- The driver of Uber bound a parameter by writing the value into the statement with a
  backslash before a quote and before a backslash, and it wrote a boolean as 1.
  Athena reads a backslash as itself, so the model set `Info.Literal` and wrote the
  filters as Trino literals. The driver of dbimp binds with the ExecutionParameters
  of the service, and writes each value as a literal that Athena parses. The
  model now uses the placeholder `?`, and `TestAthenaFilterLiterals` asserts that a
  quote, a backslash and an injection stay values.
- SHOW TBLPROPERTIES answers one text for each property, with a tab between the key
  and the value. The driver of Uber split the text, and the driver of dbimp returns it
  as the service sent it. No model reads it.

### What a second opinion found

Gemini and DeepSeek sorted the kinds that are not answered. Both put almost every kind
absent, which the survey confirmed. Their leads, each run on the service:

- Gemini named `information_schema.__internal_partitions__` as a catalog wide source
  of partitions that Athena engine version 1 had, and said it was removed in version 3.
  It answers TABLE_NOT_FOUND.
- DeepSeek named a `comment` column in `information_schema.tables`. It answers
  COLUMN_NOT_FOUND, and a table comment is only in SHOW TBLPROPERTIES.
- DeepSeek named `SELECT DISTINCT data_type FROM information_schema.columns` as a type
  list. It lists the types that columns use and no type catalog, and it is a stretch, so
  `Types` is not answered.
- Both named `"t$partitions"` and `"t$properties"` as the source of the partitions and
  the properties. Both are per table, so no statement reads them for the catalog. The
  walk of D146 is not allowed for Athena.
- `information_schema.routines` and `table_constraints` do not exist.

### What it cannot answer

Absent from Athena: indexes, constraints, sequences, triggers, user defined types,
domains, rules, inheritance, policies, access methods, tablespaces, languages, casts,
conversions, collations, operators, large objects, extensions, publications,
subscriptions and the text search kinds. An external table is not a foreign table,
because it has no server. Access is by IAM and Lake Formation, so there is no role.

Present and not answered, because only a statement that a SELECT cannot read holds
them: the comment of a table (`Comments`), the functions and aggregates of the engine,
the partitions of a table, the settings of the session, and the privileges, which
Athena refuses. This is in the backlog.

### What the fixture builds

`models/athena/fixture` builds its tables in the Glue database dbmeta, because the test
account cannot create one. It holds author with a table comment and two column
comments, book, region and shipment, the view recent, a Hive table partitioned by two
columns with one partition added, a table that CTAS makes and an Iceberg table
partitioned by `day(created)`. The tables are external and hold no data, so the setup
writes no file, except the one row that CTAS writes under `results/dbmeta/tables/`,
because the workgroup forces the output location and refuses `external_location`. The
location of each table is under `tables/dbmeta/` of the bucket, which the account
allows. The teardown drops only what the setup made, because the Redshift Spectrum test
needs `spectrum_t` in the same database.

It builds no constraint, index, sequence, trigger, type or role, because Athena has
none. Every column is nullable and has no default, because a Glue column has neither.

### What the conformance test says

The section holds author, book, region, shipment and the view recent. Athena has no
key, no unique constraint and no check constraint, so every constraint line is missing,
the key columns have no `not null` and no primary key, and so athena is in
`agreementExcluded` with that reason. The view recent reads nullable columns, as MySQL
does not.

### Which answers depend on who is asking

The reader is an IAM user that can query the Glue database and cannot create or drop a
table or write under `tables/`. It gets the administrator's answer to every query,
`CurrentUser` included, because `current_user` is the number of the account for both.
No query is refused and none reads fewer rows. `TestPrivilegeParity` records it, and
the section `athena/same/reader` is empty. A principal that Lake Formation hides
tables from was not measured.

### The cost

Athena bills the bytes that a query scans, with a minimum of 10 MB, and the workgroup
caps a query at 100 MB. A query of INFORMATION_SCHEMA scans no data, so the cost of
a pass of the eight statements is time. The fixture writes one row. The statements read
each view once and name no outer row in a subquery, so the cost grows with the number
of tables and not with their square. A catalog with thousands of tables was not built,
because a table is a job and takes seconds.

## Databricks

`models/databricks` answers 17 of the 65, in the SQL of Databricks SQL on Unity
Catalog, measured on 2026-10-11 on the hosted service, a serverless SQL
warehouse of the free edition on AWS, with databricks-sql-go v1.16.0. The tests
moved on 2026-10-11 to `github.com/xo/dbimp/databricks` v0.17.0, which dburl v0.50.0
names. The driver returns an array as a slice where the earlier driver returned its
JSON text, and that changed one test and no model. Conformance and parity did not
change. See D224 and D229.

Databricks is a hosted service and the model reads the release of its SQL channel,
2026.38, so the tier is Verified and CI never runs it. The tests of the model need
the workspace, and they skip when `DBMETA_DATABRICKS` is not set.

### Which catalog it reads, and why

The INFORMATION_SCHEMA of the catalog that the connection is in. Unity Catalog has
a catalog, which holds schemas, which hold tables, views, volumes and functions.
The model calls the catalog a database, as BigQuery does, and the statements write
`information_schema.tables` with no catalog in front. Databricks resolves that name
to the current catalog, so a connection reads one catalog, and a statement cannot
name another one, because a statement cannot bind a name. The catalog and the schema
of the connection come from the URL, `?catalog=workspace&schema=dbmeta`.
`system.information_schema` spans every catalog, and the model reads it for one thing
only: `Databases` lists `system.information_schema.catalogs`, because the view of the
session lists only its own catalog.

INFORMATION_SCHEMA shows a principal only the objects that it can use. The model
reads 19 views of it: CATALOGS and CONNECTIONS of the system catalog, and SCHEMATA,
SCHEMA_PRIVILEGES, TABLES, TABLE_PRIVILEGES, TABLE_TAGS, COLUMNS, VIEWS,
TABLE_CONSTRAINTS, KEY_COLUMN_USAGE, REFERENTIAL_CONSTRAINTS, ROUTINES, PARAMETERS,
ROUTINE_COLUMNS, ROUTINE_PRIVILEGES, VOLUMES, ROW_FILTERS and COLUMN_MASKS of the
catalog, and the table function `collations()`.

### What it answers

Databases, schemas, tables, columns, views, constraints, constraint columns,
functions, routine parameters, comments, partitioned tables, privileges, policies,
collations, foreign servers, the current schema and the current user.

A table is a managed table, an external table, a clone, a view, a materialized view,
a streaming table or a foreign table. `Table.Type` spells them table, external table,
foreign table, view, materialized view and streaming table, and a clone is the type
of the table it copies. `Table.Owner` is TABLE_OWNER, which is a group, a user or the
application id of a service principal. `Table.AccessMethod` is the data source format,
such as delta. `Table.Options` holds the storage path of a table that has one, and a
`tag.name=value` for each tag. `Table.RowSecurity` is true for a table with a row
filter. The size and the rows are absent, because only DESCRIBE DETAIL has them. A
materialized view makes a hidden table and an event log table in its schema, and both
are listed as tables.

COLUMNS counts the ordinal from zero, and the model adds one. `Column.DataType` is
FULL_DATA_TYPE, such as `bigint` or `array<string>`, and not DATA_TYPE, which is the
short name LONG. The partition columns are in the order that the table was made with.
COLUMNS reports no default, no identity and no generated column for the columns that
have them, and no collation, so those fields are absent. The tests assert that.

Databricks has a primary key and a foreign key, and it never enforces either, so
`Constraint.Enforced` is false for each. IS_DEFERRABLE and INITIALLY_DEFERRED say YES
for every constraint, which the model passes on. It has no unique constraint. A CHECK
constraint of Delta is a table property, `delta.constraints.<name>`, and neither
TABLE_CONSTRAINTS nor CHECK_CONSTRAINTS lists it, so `Constraints` does not return it.
The statement of a constraint is not kept anywhere, so the model builds `Definition`
from its columns, as `FOREIGN KEY (country, area) REFERENCES dbmeta.region (country, area)`.
A clone copies the keys of its source under names of its own, such as `author_clone_pk`.

A routine is a function or a procedure. A table function is a function whose result
type is a list of columns, and ROUTINE_COLUMNS holds them, so `RoutineParameters`
reports each as a parameter with the mode `table`, after the real parameters. A
procedure has IN and OUT parameters. A function in Python has the language python and
a body, and one declared DETERMINISTIC is immutable. A function is not overloaded.

`Privileges` has a row for each relation, with one line for each grant as
`grantee=PRIVILEGE/grantor`, and the line says when the relation inherits the grant from
its schema or its catalog. A relation with no grant has an absent value, which means
the owner alone holds it. `Schemas` does the same for a schema and `Functions` for a
routine. A grantee is a name, a group or an application id, and a privilege spells a
space as an underscore, such as USE_SCHEMA.

### What the models call analogues

- A connection of Lakehouse Federation is a foreign server. CREATE SERVER is a synonym
  of CREATE CONNECTION. The account cannot create one, so the query was shown to run
  and to return no row, and its columns were read from the header.
- A row filter and a column mask are policies. A row filter has the command all and a
  column mask has the command select. Neither has a name of its own, so the name is the
  function, and the table lists the function and the columns it takes.
- A partition column of a Delta table is a partition by the value of the column, so
  the strategy is list, and a table partitioned by two columns has a row for each.
- A catalog is a database.

### What a second opinion found

Gemini and DeepSeek sorted the unanswered kinds. Both failed on the long question.
Gemini timed out, and DeepSeek returned nothing because its reasoning used every token,
and each answered a short question. Every lead was run on the workspace.

- Gemini said that the default, the identity and the CHECK constraint are in COLUMNS and
  CHECK_CONSTRAINTS and that the service reserves them. That is right: the views have the
  columns, and they report NULL, NO and no row for objects that have them.
- DeepSeek named five relations that do not exist:
  `system.information_schema.table_sizes`, `roles`, `applicable_roles` and `sequences`,
  and a column `clustering_columns` of TABLES. Each answers TABLE_OR_VIEW_NOT_FOUND or
  UNRESOLVED_COLUMN. DeepSeek and Gemini both put the table size in DESCRIBE DETAIL,
  which is right and is a statement.
- DeepSeek named external_locations as a tablespace and shares as publications. Both
  relations exist and hold no row on this account, which cannot create an external
  location or a share, so no answer could be checked and the model leaves both out.
  They are leads for an account that holds some.
- The privileges of the system catalog, `system.access` and `system.storage` refuse
  the principals with INSUFFICIENT_PERMISSIONS, so the history of queries and the
  storage statistics are out of reach.

### What it cannot answer

Absent from Databricks: indexes, index columns, sequences, triggers, user defined
types, enumerated types, domains, extensions, rules, inheritance, partitions,
access methods, tablespaces, conversions, casts, languages, large objects, event
triggers, operators, operator classes, the text search kinds, foreign data wrappers,
foreign tables, user mappings, publications, subscriptions, extended statistics,
column privileges, default privileges, aggregates and not null constraints. A NOT NULL
is a property of a column and has no name, so it is in `Column.Nullable`. Delta has no
index, and liquid clustering and Z ordering are not indexes. A Delta partition is a
directory of files and not a table, and SHOW PARTITIONS lists it as a statement.

Roles, role grants and role settings are absent from SQL. A user and a group belong to
the account, and only the SCIM API lists them. A grantee appears in `Privileges` as a
name, and that is all that SQL shows.

Present and not answered, because only a statement that a SELECT cannot read holds them:
the size, the row count, the location and the clustering columns of a table
(DESCRIBE DETAIL), the CHECK constraints (SHOW TBLPROPERTIES), and the default, the
identity and the generated columns (SHOW CREATE TABLE). The settings of the session
are SET. This is in the backlog.

Present and not read, because they need something that the account lacks: the
volumes and the routines of a principal that holds no READ VOLUME and no EXECUTE are
hidden by INFORMATION_SCHEMA, the external locations and the shares are empty here, and
CATALOG_PROPERTIES answers that it is not supported.

### What the fixture builds

`models/databricks/fixture` builds its objects in the schema `workspace.dbmeta`, which
the administrator owns and cannot recreate. It holds author with a table comment, a
column comment, a key, a default and a tag, book with a primary key, a foreign key and a
CHECK constraint, region and shipment with a composite key, the view recent, sales
partitioned by two columns, clicks with liquid clustering, ticket with an identity
column and a generated column, a shallow clone of author, a volume, a grant to
`account users`, six functions, a procedure, a row filter and a column mask. It builds
no materialized view, because DROP MATERIALIZED VIEW leaves two tables behind and the
pipeline spends the daily quota. The teardown drops what the setup made and never the
schema.

### What the conformance test says

The section holds author, book, region, shipment and the view recent. Databricks
has no unique constraint, and its CHECK constraint is a table property that no relation
lists, so the lines for the unique title and the check of book are missing. The key
columns agree with PostgreSQL: not null, with the primary key, and the foreign keys
point at the same columns. `author.shade` reads no default, because COLUMNS reports
none, and the columns of the view recent read not nullable, where PostgreSQL and MySQL
read nullable. So databricks is in `agreementExcluded` with that reason.

### Which answers depend on who is asking

The reader is a service principal that holds USE CATALOG on the catalog and USE SCHEMA
and SELECT on the schema, and cannot create or change anything. Four answers differ in
`databricks/same/reader` of `TestPrivilegeParity`, and none is refused.

- `Functions` and `RoutineParameters` return no row to the reader. INFORMATION_SCHEMA hides
  a routine from a principal that holds no EXECUTE on it, so a query that asks as the
  reader and finds no function has found no function that the reader can run, and not
  no function. The administrator reads all seven.
- `Comments` returns fewer rows, because the comment of a function and the comment of the
  volume go with them. The volume is hidden from a principal that holds no READ VOLUME.
- `CurrentUser` has the same row with a different value, the application id of each
  service principal.

Every other query, `Tables`, `Columns`, `Constraints`, `Privileges`, `Policies` and
`Databases` among them, answers the same to both. `Privileges` shows the grants of the
whole schema to the reader, because it is the grantee, and the same rows to the
administrator. A principal that holds EXECUTE and READ VOLUME, and one that holds no
SELECT on a table, were not measured.

### The cost

The warehouse is a 2X-Small serverless warehouse, shared with dbimp, that stops after
ten minutes. The first statement after a stop waits for it to start, up to a minute,
and the tests wait for it. The free edition has a daily quota of compute, and a run of
the tests takes about ten minutes. Each statement reads a view of INFORMATION_SCHEMA once
and names no outer row in a subquery, so the cost grows with the number of objects and
not with their square. A catalog of thousands of tables was not built, because the
quota allows no such run, so the plan of each statement against a large catalog was
not checked.

## Releases that need a license file

Stardog, GraphDB and Volt Active Data do not start without a license file
that a person downloads, and `dbrun` lists them only while it finds the file.
dbmeta has no model for any of them, so every release of the three is Staged,
and CI never runs one. See D118 and D119.

| Product | Releases | Measured |
| --- | --- | --- |
| Stardog | 12.1.5, 12.2.0 | Not yet. No license file is provisioned |
| GraphDB | 11.4.3, 11.5.2 | Not yet. No license file is provisioned |
| Volt Active Data | 14.1.0, 15.2.0 | Not yet. No license file is provisioned |

Put each file at `$XDG_CONFIG_HOME/dbmeta/licenses/<product>`, where the
product is `stardog`, `graphdb` or `voltdb`, or name its path in
`DBMETA_<PRODUCT>_LICENSE`. `docs/DBRUN.md` says the same under License files.

## Apache Avatica

`models/avatica` answers 24 of the 65 on the standalone Avatica server 1.28.0 and
1.29.0, measured on 2026-10-08 through dbimp's avatica driver, which is what
dburl's avatica scheme opens. Both releases are Tested. Avatica is the wire
protocol of Apache Calcite, and the standalone server runs HSQLDB 2.4.1 in
memory, so the model reads the catalog of HSQLDB. D186 holds the mapping and the
reasons for it, for Ken to review.

### What it reads

HSQLDB has an INFORMATION_SCHEMA of the SQL standard, with 64 views, and 27 more
whose names begin with SYSTEM_. The SYSTEM_ views hold the metadata of JDBC, such
as SYSTEM_INDEXINFO, SYSTEM_COMMENTS and SYSTEM_PROPERTIES. Every query is one
statement over them, and no kind is a walk (D146). A view that HSQLDB computes
each time it is read is slow to join, so three statements read a derived table or
a grouped union where a plain join took seconds. D186 has the numbers.

### What it answers

Databases, schemas, the current schema, the current user, tables, views, columns,
indexes, index columns, constraints, constraint columns, triggers, sequences,
functions, aggregates, routine parameters, types, domains, collations, comments,
settings, roles, role grants and privileges.

The catalog is always PUBLIC. A table has a type that says where HSQLDB keeps the
rows, such as `memory table`. A NOT NULL is a check named SYS_CT and a number, so
Constraints lists one for every such column. A key makes an index named SYS_IDX_
and a number, and the number changes between runs. Privileges are one row for
each object, and every object has the grants of its owner. The aggregate has no
column that says so, and the definition text does.

The version is the release of HSQLDB, which is 2.4.1 on both releases, and not
the release of Avatica. usql reads it with the same statement.

### What it cannot answer, and why

32 kinds. HSQLDB has no tablespace, access method, language, conversion, cast,
event trigger, role setting, default privilege, foreign data wrapper, foreign
server, user mapping, publication, subscription, text search object, operator
object, extension, extended statistic, partitioned table or enumerated type.
Four are analogues that were found and not answered:

- Large objects. SYSTEM_LOBS.LOB_IDS lists the LOB values that are stored, one row
  for each value, with a length and a count of uses. A row is data and not an
  object, and only an administrator can read it, so this follows D162.
- Foreign tables. A text table keeps its rows in a CSV file, as file_fdw does,
  and SYSTEM_TEXTTABLES lists them. It has no server and no wrapper, so the four
  foreign kinds stay unanswered. The fixture cannot build one on an in memory
  server.
- Tablespaces. SYSTEM_TABLESTATS has a SPACE_ID. It is NULL for every table on an
  in memory server, so no row could be checked.
- Column statistics. SYSTEM_TABLESTATS has a row count and the space of a table
  and no statistic of a column.

### What a second opinion found

Gemini and DeepSeek were asked on 2026-10-08, as hard rule 14 requires. Gemini
Flash timed out on all five tries, and Gemini Pro answered. DeepSeek spent its
whole budget on reasoning in three tries, and answered the fourth with a limit of
20,000 tokens. Each lead was run against 1.29.0.

| Lead | Who | Result |
| --- | --- | --- |
| SYSTEM_TABLESPACES holds tablespaces | DeepSeek | Invented. The view does not exist |
| SYSTEM_LANGUAGES holds languages | DeepSeek | Invented. The view does not exist |
| SYSTEM_JARS holds jars | DeepSeek | Invented. JARS exists, and it is empty and has no kind here |
| SYSTEM_LOBS holds large objects | DeepSeek | A schema of the LOB store and not a catalog. LOB_IDS lists values. Not answered |
| SYSTEM_TEXTTABLES holds foreign tables | both | The view exists and is empty. A stretch, as above. Not answered |
| SYSTEM_SYNONYMS holds synonyms | DeepSeek | The view exists. No kind lists a synonym, and HSQLDB 2.4.1 cannot create one |
| SYSTEM_TABLESTATS holds tablespaces | Gemini | A stretch. SPACE_ID is NULL on an in memory server. Not answered |
| Casts, text search, extensions, column statistics, partitioned tables, enums | both | Held as absent |

### What the fixture builds

`models/avatica/fixture` is SQL, because HSQLDB takes DDL and the driver runs it.
It makes the four core tables of D53 and the view recent. It adds a table with an
identity column and a generated column, a global temporary table, a sequence, a
domain, a distinct type, a function, a procedure, an aggregate, a trigger, two
roles with grants to them, and a comment on a table, a view and a column. It
cannot build a text table, a cached table, a collation of its own or a comment on
a routine, a sequence or an index, and the package comment says why.

### What the conformance test says

Avatica agrees with PostgreSQL on the whole core schema, and it needs no entry in
the list of differences. A table is a table, recent is a view, every key and
every foreign key is there, and a primary key column reads NOT NULL. A NOT NULL
check has no row in the constraint columns, so the report holds none.

### Which answers depend on who is asking

HSQLDB hides what a user cannot access and refuses no statement. The user that
the entry makes can read one table, DBMETA.READABLE. It sees that table and its
columns, three schemas, itself among the roles, and the settings. SCHEMATA lists
only the schemas a user owns, so the owner of every schema is empty for it. The
grantee that holds the role dbmeta_reader sees the table author, its columns, the
two roles and the sequence. It does not see the procedure it may execute.
The sections `avatica/same/user` and `avatica/same/grantee` of
`test/testdata/parity.txt` hold the rest. Every user can read the version and the
settings.

### What stays out, and why

Phoenix speaks the same protocol and has no model. Its catalog, its version, its
terminator and its users differ, and D186 says what a Phoenix dialect would need.
### The describe fields (D210)

Tables reads the owner from `SCHEMATA`, which is the owner of the schema and is NULL for another user's schema, and the persistence from `TABLE_TYPE`. The rows are NULL: `SYSTEM_TABLESTATS` holds `CARDINALITY`, and its join grows with the square of the tables. The size is NULL for a memory only database.


## Azure Cosmos DB

`models/cosmos` answers 15 of the 65, with the API for NoSQL, measured on
2026-10-11 on the hosted account with the driver of dbimp, `github.com/xo/dbimp/cosmos`.
That is the driver that dburl v0.50.0 names for the cosmos scheme, so D154 is met. D228
built the model on v0.17.0 with 10 kinds. D233 adds five kinds from the five statements that
dbimp wrote for this model. They are in dbimp v0.20.0. D223 left Cosmos DB without a model because the driver then read only
documents, and D228 builds the model now that the driver reads the catalog.

Cosmos DB is a hosted service with no release, so the tier is Verified and CI never
runs it. The tests of the model need the account, and they skip when `DBMETA_COSMOS`
is not set. The emulator entry in `container/cosmos.go` stays Staged. The model is
written against the hosted service, and the emulator leaves out the conflict
resolution policy, the geospatial type and the unique keys of a container in the
list of containers.

### Which catalog it reads, and why

The SQL of Cosmos DB reads the documents of one container and nothing else, so no
SELECT reads a catalog. A database, a container, its policies, its scripts, its users
and its throughput are resources of the REST API. The driver of dbimp answers a
SELECT against a reserved name with GET requests on the matching resource, and the model
reads twelve of the names and no other. A statement is
`SELECT * FROM "$containers"`, with no list of columns and no LIKE.

| Reserved name | The resource it reads | Request units |
| --- | --- | --- |
| `$account` | the account, `GET /` | not read by a kind |
| `$databases` | `GET /dbs` | 2 |
| `$containers` | `GET /dbs/{db}/colls` | 2 |
| `$stored_procedures` | `GET /dbs/{db}/colls/{c}/sprocs` | 1, not read by a kind |
| `$triggers` | `GET /dbs/{db}/colls/{c}/triggers` | 2 |
| `$functions` | `GET /dbs/{db}/colls/{c}/udfs` | 2 |
| `$offers` | `GET /offers`, and the database and its containers, to name each offer | 2 for the offers |
| `$users` | `GET /dbs/{db}/users` | 2 |
| `$permissions` | `GET /dbs/{db}/users/{u}/permissions` for each user | 2 for each user |
| `$index_paths` | `GET /dbs/{db}/colls/{c}` | not measured |
| `$unique_keys` and `$unique_key_paths` | `GET /dbs/{db}/colls/{c}`, one request for each statement | not measured |
| `$routines` | `GET .../sprocs` and `GET .../udfs` | not measured |
| `$partition_key_ranges` | `GET .../pkranges` | 0 |
| `$session` | no request | 0 |

The request units are the charge that the account reported for the same GET requests,
measured on 2026-10-11. A feed that has more than one page costs that for each page.

The statement names no database and no container. The driver takes both from the path
of the URL, `cosmos://x:key@account.documents.azure.com/database/container`, where the
user is any word and the account key is the password. So a connection reads one
database, and the routines, the triggers, the index paths, the unique keys and the
partition key ranges of the one container that the URL names. A URL with no container
makes the driver refuse those statements before it sends a request, and the tests read that
refusal. `$session` needs neither. The test URL names the database
`dbmeta` and the container `book`.

The grammar of the driver can name a database in the WHERE of a statement, and the
model does not use it. A parameter of dbmeta is a pattern and the key of the driver
takes an exact name. A statement for each database is a walk, and Ken chose on
2026-10-11 that no walk is allowed for Cosmos DB (D223).

Because a statement cannot filter, the `Keep` function of each binding narrows the rows
in Go with `dbmeta.Like`, as the Cassandra model does (D200). The fields of a binding
are the columns of the driver, in its order, and `Scan` reads them by name. A list
arrives as a `[]any`, a policy as a `map[string]any` and a number as an `int64`.

### What it answers

Databases, schemas, tables, indexes, index columns, constraints, constraint columns,
partitioned tables, partitions, functions, triggers, roles, privileges, settings and the
current schema. Every one is an analogue, and the next section says why.

A Cosmos DB database holds containers, users and permissions, and nothing lies between
it and the account. So it is the database of `Databases` and also the schema of
`Schemas` and of every other kind, the way an ArangoDB database is (D168). The catalog
of every row is empty. Both kinds list every database of the account, and `Tables`
lists the containers of the database that the URL names. `Schema.Options` holds the
`rid` and the `link` of the database.

A container is a table of the type `container`. It has no owner, no size, no row count
and no comment that the feed reports. `Table.Options` holds the settings that no field
has a place for, as name=value pairs: `rid`, `link`, `partition_key` with the paths
joined by a vertical bar, `partition_key_kind`, `default_ttl`, `geospatial_type`, the
conflict resolution mode, path and procedure, `unique_keys` with the paths of one key
joined by a plus sign and the keys joined by a vertical bar, `computed_properties` with
the names, and `full_text` and `vector_embedding` when the policy is set. A setting
that the container does not have is left out, and `default_ttl` is absent for a
container with no time to live, which is not a time to live of zero.

The indexing policy is the one index of a container, with the name `indexing_policy`.
Its type is the indexing mode, `consistent` or `none`. Its options hold `automatic` and
the included and the excluded paths, and its definition is the whole policy as JSON
text, with the composite, spatial and vector indexes in it. The index is never unique
and never primary.

A container is split by the hash of its partition key, so every container is a
partitioned table with the strategy `hash`, and the expression is the paths of the key.
A range of the partition key, which is a physical partition, is a partition. Its name is
the id of the range, its type is `partition`, and its bound is `FROM ('min') TO ('max')`
with the two ends as hexadecimal text. The status and the parents of a range have no
field and are not returned.

The paths of the indexing policy are the index columns. Every row names the one index
`indexing_policy`. `IndexColumn` has no field for the kind of the entry, so `Expression`
says it: `included`, `excluded`, `composite 0`, `spatial Point,Polygon`, `vector flat 128`
or `full_text`. A path of a composite index has its position from 1 as the ordinal and its
order in `Descending`. Every other path has the ordinal 1 and no order. The statement reads
the one container of the URL. The hosted service lists all four geometry types for a spatial
path that names Point.

A unique key is a constraint of the type `unique`. A key has no name, so the name is
`unique_key_` and its position from 0. The paths of the key are the constraint columns, in
order, with the ordinal counted from 1. The statements read the one container of the URL.

A user defined function is a function, with the language `javascript` and its body as
the source. A stored procedure is a function of the kind `procedure`, so `Functions` reads
the two in one statement, `$routines`. The id of a routine is its resource id, because a
procedure and a function of one container can share a name. The etag and the time of the
last change have no field. A trigger is a trigger of its container. Its definition is the resource as
the REST API holds it, as JSON with the id, the body, the trigger type and the trigger
operation, because a trigger has those beside its body and no field holds them. A
trigger has no enabled state, because a request names it, so `Enabled` is always empty.

A user of a database is a role. It has no password and no attribute, so every flag is
false and the connection limit is -1. A permission is a privilege, one row for each
permission. Its name is the resource link, which is rid based, because the feed names
the resource by nothing else. Its type is the kind of resource that the link names,
and its access is the user, an equals sign and the mode, such as `reader=Read`.

The provisioned throughput of the database, and of each container that has its own,
is a setting named `throughput` and the database, with a slash and the container for
a container offer. Its value is the manual request units a second, or the largest
request units of an autoscale offer. Its type is `manual` or `autoscale` and its
context is the scope.

### What the models call analogues

Each is a choice that Ken can reverse:

- A database is both a database and a schema.
- A container is a table.
- The indexing policy is the index of a container. A container has no index with a
  name, and the policy holds many paths. Its paths are the index columns, with the kind of
  the entry in `Expression`. An index for each entry was not chosen, because a binding
  returns one object for each row and a composite index has several rows (D233).
- A unique key is a unique constraint with a name made of its position.
- A range of the partition key is a partition.
- A stored procedure is a function of the kind procedure.
- The database of the URL is the current schema.
- A hash partition key makes a container a partitioned table.
- A user defined function is a function, and a trigger is a trigger.
- A user is a role, and a permission is a privilege.
- A throughput offer is a setting. It is the nearest thing to a configuration value
  that the service lists. It was a choice between that and the account, and the account
  is one row of ten columns, which no kind of one setting for each row can hold.

### What a second opinion found

Gemini and DeepSeek sorted 21 kinds before the model, and D223 records that they agreed
on every one of them except the path of the role feed. Every lead was a REST path. The
leads that the model reads are the feeds of databases, containers, scripts, throughput
and users, which the hosted account answered. The leads that had no recording were run
on 2026-10-11:

- `GET /roledefinitions`, the role feed that DeepSeek named, is HTTP 401 "RBAC Request
  must be signed by a system key". It is the role based access control of the account,
  which the key of the account cannot read.
- `GET /dbs/{db}/roledefinitions` and `GET /dbs/{db}/roles`, the paths that Gemini
  named, are HTTP 400 "Request url is invalid". There is no role feed that a key reads,
  so Roles reads the users.
- `GET .../materializedviews` is HTTP 400 "Request url is invalid", and so is
  `GET .../attachments`. A materialized view is not a resource of this API.
- `GET .../pkranges` answers the physical partitions of a container for no request
  units. The driver has no statement for it, so Partitions is not answered.
- `GET /dbs/{db}/colls/{c}/conflicts` and `GET /dbs/{db}/clientencryptionkeys` answer
  empty feeds. Neither has a kind among the 65, and the driver has no statement for them.
- A vector embedding policy and a vector index are refused with "the capability has not
  been enabled on your account". A change feed retention is refused unless the account
  keeps all versions. Both columns of `$containers` read NULL on this account.
- A full text policy, a computed property and a spatial index are accepted, and the
  fixture builds each one. A geometry spatial index needs a bounding box.

### What it cannot answer

50 kinds.

- Columns, column statistics and the rest of what needs a column. A document has no
  declared attribute. A sample of the documents can infer one, and the sample is a read
  of the data, which D47 does not allow. A computed property is the nearest thing to
  a generated column, and a container has many of them.
- The current user. The session names the host, the database and the container, and no
  user. The account has two keys, and no statement says which one a connection holds, so
  the current user has no source. A test asserts that no column of `$session` names a
  user (D233).
- Sequences, views, foreign keys, types, domains and the rest are absent from the
  product. A view is not a Cosmos DB object, and a document has no foreign key.
- Comments. A container has no comment.

### What the fixture builds

The database `dbmeta` of the account, with the 400 request units a second that its
containers share. Four containers, which are the core tables of D53 that a document
store can hold: `author` with a full text policy and a full text index, `book` with two unique keys, an
indexing policy that includes two paths and excludes the rest and a composite index,
`region` with no indexing, and `shipment` with a time to live of thirty days, the
geometry type, a spatial index and a computed property. The container `book` holds a
function, a stored procedure and a trigger. Two users, `reader` and `auditor`, hold a
read permission on `book` and all permissions on `author`.

The fixture is a list of REST requests, and `test/cosmos_rest_test.go` signs and sends
them with the key of the account, because the SQL of Cosmos DB writes nothing and the
driver reads only. The tests make no database and drop every container and user
that they made when the run ends. They never touch the other database of the account.

It cannot build a vector embedding policy, a vector index or a change feed retention,
because the account has neither capability. It builds no conflict resolution policy,
because only an account with several write regions takes one. It builds no analytical
store. A container with two partition key paths is refused at the version of the API that
the driver names. A database has no permission of its own either, because the server
refuses a resource that is a database, so a permission is on a container.

### What the conformance test says

The section of Cosmos DB holds the four containers as tables and nothing else. It is in
`agreementExcluded`: a document has no column, so there is no column catalog (D228). The
unique keys, the index paths, the partitions and the routines are read by tests of their
own, because each statement reads the one container that the URL names (D233). The section
did not change when the five kinds arrived.

### Which answers depend on who is asking

Two principals, both with the key of the account: the primary key of the administrator
and the read-only key of `cosmos-reader`. A key is account wide, so neither is limited
to one database. `TestPrivilegeParity` records one difference. The read-only key is
refused HTTP 401 by the feeds of users and permissions, so Roles and Privileges are
refused to it ("The MAC signature found in the HTTP request is not the same as the
computed signature"). It reads every other feed that the model reads, with the same rows.
A user of a database is not a way to connect here, because it holds a resource token for
an application and the driver takes only a key.

### The cost

A feed costs 2 request units for a page of the lists that the model reads, and the three
script feeds cost 1 or 2. A statement is one request, and `$permissions` is one request
for the users and one for each user. `$offers` reads the offers and then names each one.
The 400 request units a second that the database shares were enough for the whole test run,
and no request was refused for rate. No catalog with thousands of containers was built,
because a database with shared throughput holds a few tens of containers at the
free tier, so the plan was not checked against one. The cost grows with the number of
containers and users and with the pages of a feed, never with the number of documents.

---

--- METRICS ---
- Files Extracted: 6
- Estimated Token Budget: ~132036 tokens
- Canonical Reference: https://codewiki.google/github.com/xo/DBMETA
