Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
6f6577b
fix(db): name an unnamed or repeated result column so every column ke…
cevheri Oct 7, 2026
3708a14
fix(mssql): read user results positionally so unnamed and repeated co…
cevheri Oct 7, 2026
e3e7ef3
fix(mssql): assert zoneless conversion in later result sets and retit…
cevheri Oct 7, 2026
70c412e
fix(db): read PostgreSQL and MySQL results by position so a repeated …
cevheri Oct 7, 2026
69d3b85
fix(db): pin the MySQL empty column name and correct the result-shape…
cevheri Oct 7, 2026
3635909
fix(sqlite): read result rows by position so a repeated or empty colu…
cevheri Oct 7, 2026
935c2ae
fix(duckdb): read result rows by position so a repeated column keeps …
cevheri Oct 7, 2026
3b2ebf4
fix(libsql): name result columns uniquely so a repeated or unnamed co…
cevheri Oct 7, 2026
9730e6c
fix(cassandra): name an empty result column and refuse two columns of…
cevheri Oct 7, 2026
f5a8d2d
fix(db): number repeated columns with the shared helper in Druid, sea…
cevheri Oct 7, 2026
2d9f170
fix(db): refuse a row whose value count differs from its columns in D…
cevheri Oct 7, 2026
3ab5ca3
fix(db): name a search column declared without a name as an unnamed c…
cevheri Oct 7, 2026
15690e2
fix(panel): recover the results panel from a render error and name it…
cevheri Oct 7, 2026
177174e
fix(grid): refuse inline edit on a column the result named itself
cevheri Oct 7, 2026
76625b1
fix(masking): mask a repeated column the result numbered like the col…
cevheri Oct 7, 2026
77475b5
fix(grid): refuse to apply an edit to a generated column in the editi…
cevheri Oct 7, 2026
e7d841c
fix(graph): name only a failed package import a chunk failure
cevheri Oct 7, 2026
504fdb2
docs(providers): use a colon where two rewritten lines kept a dash
cevheri Oct 7, 2026
6f83560
fix(document): name an empty document key so the grid can take its co…
cevheri Oct 7, 2026
5b66c78
fix(grid): read the numbered column form with one parser beside its p…
cevheri Oct 7, 2026
47f950b
docs(db): make the result-name docblocks true of the code again
cevheri Oct 7, 2026
43d3ae9
fix(db): key PostgreSQL, MySQL and Db2 rows through keyRowsByPosition
cevheri Oct 7, 2026
9f66781
fix(results): keep a render error's message off the screen
cevheri Oct 7, 2026
85bf246
docs(elasticsearch): point the row rebuild citations at toRow and reb…
cevheri Oct 7, 2026
7da6f9a
fix(clickhouse): number a column the server declares twice with the s…
cevheri Oct 7, 2026
0081dca
fix(influxdb3): refuse a SQL result whose line names a column twice
cevheri Oct 7, 2026
38dba90
fix(clickhouse): read result rows by position so a repeated column ke…
cevheri Oct 7, 2026
aadd47f
fix(influxdb3): say "more than one column" for a repeated name and do…
cevheri Oct 7, 2026
33ae2c3
docs(db): name JSONCompact in the ClickHouse comments and state when …
cevheri Oct 7, 2026
610ce31
docs(api): move two postgres.ts citations to the lines the keying cha…
cevheri Oct 7, 2026
8f2fc5c
test(sqlite): collect before removing the names fixture so windows-la…
cevheri Oct 7, 2026
c8ca93b
fix(neo4j): name an empty output column (No column name)
cevheri Oct 7, 2026
5b92a8e
fix(influxdb3): name an empty column (No column name)
cevheri Oct 7, 2026
76c8a21
fix(db): number repeats in uniqueFieldNames in one pass
cevheri Oct 7, 2026
bc13834
fix(studio): reset the panel boundary when the schema or the connecti…
cevheri Oct 7, 2026
04ef375
fix(mssql): carry the statement in the positional result refusals
cevheri Oct 7, 2026
19ee34c
fix(search): refuse a declared column name that is not text
cevheri Oct 7, 2026
2356a30
style: move the masking import to the top and follow the repository's…
cevheri Oct 7, 2026
3c37f77
test(masking): pin a numbered repeat to its base's rule and a look-al…
cevheri Oct 7, 2026
2e9117a
docs(oracle): state how a repeated and an unnamed column come back, a…
cevheri Oct 7, 2026
baa9d38
fix(oracle): name result columns from the declared names, a repeat as…
cevheri Oct 7, 2026
7685198
fix(search): refuse a declared column alias that is not text
cevheri Oct 7, 2026
b9f11a4
docs(backlog): record the stale line citations found in the PR #1575 …
cevheri Oct 7, 2026
5cd1fa1
Revert "fix(oracle): name result columns from the declared names, a r…
cevheri Oct 7, 2026
f0e1b3f
docs(oracle): keep the driver's NAME_1 naming and say why the declare…
cevheri Oct 7, 2026
9967411
fix(libsql): key declared column types as own properties so __proto__…
cevheri Oct 7, 2026
25319f2
Merge remote-tracking branch 'origin/main' into fix/result-column-names
cevheri Oct 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2757,8 +2757,8 @@ Neither `StudioModals`, `StudioOverlays` nor the modals are memoized.
### X9. What `columnTypes` still cannot name, measured

The four string-returning drivers fill `QueryResult.columnTypes` since 2026-08-23, and
SQLite joined them on 2026-09-18 by reading its own declarations through the driver bridge. Four bounds were
measured while doing it, and each is a small residue rather than a defect:
SQLite joined them on 2026-09-18 by reading its own declarations through the driver bridge. Three bounds
measured while doing it remain open, and each is a small residue rather than a defect:

- **A user-defined type has no name.** Postgres's built-in OIDs are a generated static table (they are
compiled into the server and never reused), so an enum, a composite or an extension type falls
Expand All @@ -2774,11 +2774,6 @@ measured while doing it, and each is a small residue rather than a defect:
hands a bit string back as the string `"1010"` while `mysql2` hands back a Buffer, so the same
declared name needs the text family on one engine and the binary family on the other. One name, two
answers, which is why it was left alone.
- **The mssql transaction path declares types for columns `fields` does not list.** `queryInTransaction`
takes `fields` from `Object.keys(recordset[0])`, so a zero-row result has no fields while its
`recordset.columns` (which does carry the declaration, even for zero rows - measured) fills
`columnTypes`. Harmless today because all three consumers iterate `fields`; taking `fields` from
`columns` too would be the right fix and is a behaviour change of its own.

**Done when:** each bound is closed or judged settled, with the enum case the only one a user is
likely to meet.
Expand Down Expand Up @@ -4352,6 +4347,7 @@ Two halves, and the second is what stops it recurring:
resolves each, and fails on a miss, so a coordinate cannot go stale silently again.

DOC4 is the same class in the provider docs.
More instances, found stale at `8f2fc5c81` during the PR #1575 review (2026-10-07): `tests/integration/db/mssql-provider.test.ts` (`mssql.ts:1798`), `docs/providers/opensearch.md` (`http-transport.ts` `:806`, `:883-935`, `:913`, `:999`, `:1127`, `:1128`, `:1283`), `docs/providers/elasticsearch.md` (`http-transport.ts:1194`), `docs/BACKLOG.md` (`http-transport.ts:1556`), `tests/components/studio/BottomPanel.test.tsx` (`BottomPanel.tsx:574`), `docs/BACKLOG.md` (`BottomPanel.tsx:445`), and the `oracle.ts:1492`, `:2000` and `:2015` citations in `oracle.ts`, `tests/integration/db/oracle-provider.test.ts` and `tests/api/db-objects.test.ts`.
This entry was first written as a second "D94 (proposed)" block, which reused the id of D94 and was not a heading the structure guard reads.

**Done when:** a test fails on a stale `file.ts:NNNN` anywhere under `src/`, `docs/` and `tests/`,
Expand Down
6 changes: 6 additions & 0 deletions docs/providers/cassandra.md
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,12 @@ The **declaration** drives the row shape, not the row's own keys: it is the only
the statement projected. `fieldNames: null` (a write) and `fieldNames: []` are kept apart at the
seam and both collapse to no columns for the grid.

A column declared with an empty name comes back as `(No column name)` (`uniqueFieldNames` in [`result-fields.ts`](../../src/lib/db/utils/result-fields.ts)), so the grid always has a key for it.
Two columns of one name (`SELECT id AS x, name AS x`) are refused with a query error naming the column: cassandra-driver 4.10.0 builds each row as `row[column.name] = value` (`lib/streams.js`, `parseRows`), so the earlier column's value is already gone when the row reaches this provider, and a second header would only repeat the last value.
Give each column its own alias.
The refusal covers a repeat of one column too (`SELECT a, a`), whose two values would be equal: it stays because it is explicit and never shows a wrong value, where telling the two cases apart needs a live cluster to measure against.
This is read from the driver source and pinned with the driver's row shape in `tests/unit/db/cassandra/wire.test.ts` and `tests/integration/db/cassandra-provider.test.ts`; it was not measured against a live cluster in this change, so whether a given server version accepts a repeated name at all is not stated here.

`columnTypes` carries the wire's declared type per column. Note that `text` reads back as `varchar`
and a column declared `varchar` reads back as `text` — they are one type on the wire — so this is
whatever the protocol declared rather than the DDL word. The schema tree reads
Expand Down
37 changes: 27 additions & 10 deletions docs/providers/clickhouse.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ from:
| `DatabaseProvider` slot | ClickHouse realisation | Mechanism |
|-------------------------|-------------------------|-----------|
| "Table" (the relation kind) | A table, displayed as `name` or `database.name` | `system.tables`, filtered to non-system databases |
| "Row" | One result row | JSON `data` array element |
| "Row" | One result row | JSONCompact `data` array element, read by position |
| Columns | The declared column list, types verbatim | `system.columns` / the query response `meta` |
| Primary key | The MergeTree sparse primary index | `system.tables.primary_key`, `is_in_primary_key` |
| `query(sql)` | One SQL statement | `POST /?default_format=JSON` |
| `query(sql)` | One SQL statement | `POST /?default_format=JSONCompact` |
| Indexes | Data-skipping indexes, and nothing else ([below](#indexes-are-only-the-skipping-indexes)) | `system.data_skipping_indices` |
| Foreign keys | none (ClickHouse has none) | always `[]` |
| `getOverview()` / storage | Server identity, connection counts, part sizes | `version()`, `uptime()`, `system.metrics`, `system.parts`, `system.disks` |
Expand Down Expand Up @@ -262,18 +262,26 @@ The upside is real: **the schema tree and the overview panel survive a restricte
monitoring panels and maintenance operations that need their own grant degrade
([§7](#7-monitoring--health)).

### 3.4 `default_format=JSON` as a URL parameter, never appended to the SQL
### 3.4 `default_format=JSONCompact` as a URL parameter, never appended to the SQL

`POST /?default_format=JSON` with the raw statement as the body returns the JSON envelope. User SQL
`POST /?default_format=JSONCompact` with the raw statement as the body returns the JSONCompact envelope. User SQL
is never rewritten to add a format — the editor sends exactly what the user typed.

JSONCompact and not JSON, because only a positional row keeps every value.
Its `meta` is the same as JSON's, and each `data` row is an array of values in `meta` order instead of an object keyed by column name.
Measured on 26.7.1.1315: the engine qualifies a joined column itself (`b.x`), and that name can collide with a column the statement already calls `b.x`.
``SELECT a.x, b.x, 3 AS `b.x` FROM (SELECT 1 AS x) a, (SELECT 2 AS x) b`` declares `b.x` twice; under JSON the row is `{"x": 1,"b.x": 2,"b.x": 3}` and `JSON.parse` keeps only `3`, under JSONCompact it is `[1, 2, 3]`.
The transport numbers the repeated name with the shared `uniqueFieldNames` helper and keys each row by position ([§5.2](#52-result-shaping)).
A row that is not an array, or whose value count differs from the `meta` column count, is refused with an error rather than shown under guessed names.

**An explicit `FORMAT` in the user's own SQL wins over `default_format`, live-verified.**
`SELECT 1 FORMAT TSV` genuinely comes back as TSV even with `default_format=JSON` on the URL. The
`SELECT 1 FORMAT TSV` genuinely comes back as TSV even with `default_format=JSONCompact` on the URL. The
response header `X-ClickHouse-Format` reports the format the server actually used, and the transport
branches on it before parsing the body
([`toQueryResult`](../../src/lib/db/providers/sql/clickhouse/http-transport.ts)): when it is not
`JSON`, the raw text comes back as `rawText` rather than being parsed or thrown away — the user asked
for that format deliberately. The provider then surfaces it as one synthetic column,
`JSONCompact`, the raw text comes back as `rawText` rather than being parsed or thrown away: the user asked
for that format deliberately.
That includes an explicit `FORMAT JSON`: it comes back as the server's text under `__text`, like any other format the user chose. The provider then surfaces it as one synthetic column,
`__text` (`RAW_TEXT_COLUMN`, [`index.ts`](../../src/lib/db/providers/sql/clickhouse/index.ts)),
the same convention Couchbase uses for a scalar projection.

Expand Down Expand Up @@ -757,13 +765,22 @@ that is how the rest of the application calls every provider uniformly.

| Source | `QueryResult` field | Notes |
|--------|----------------------|-------|
| `data` array | `rows` | Objects exactly as the server returned them; 64-bit integers and every `Decimal` arrive as strings ([§3.5](#35-64-bit-integers-and-decimals-are-quoted-on-purpose-to-stop-jsonparse-from-rounding-them)) |
| `meta` array | `fields` | Declared column order; `[]` when the source could not describe the rows (a non-JSON format, or a write) |
| `data` array | `rows` | One object per row, built by position from the row's array of values and keyed by the names in `fields`; 64-bit integers and every `Decimal` arrive as strings ([§3.5](#35-64-bit-integers-and-decimals-are-quoted-on-purpose-to-stop-jsonparse-from-rounding-them)) |
| `meta` array | `fields` | Declared column order, a repeated name numbered (see below); `[]` when the source could not describe the rows (a format the user chose, or a write) |
| — | `rowCount` | `rows.length` when there are rows; otherwise `mutationCount` from `X-ClickHouse-Summary`, verbatim, zero included ([§3.6](#36-writes-return-an-empty-200-body-the-row-count-lives-in-a-header)) |
| `X-ClickHouse-Summary.elapsed_ns` | `executionTime` | The server's own duration, preferred because it excludes network latency; falls back to the envelope's `statistics.elapsed` (seconds), then to the measured wall clock when neither source reported anything |
| a non-JSON `X-ClickHouse-Format` | `rows` / `fields` | One synthetic row `{ __text: "<raw body>" }` under the single column `__text` ([§3.4](#34-default_formatjson-as-a-url-parameter-never-appended-to-the-sql)) |
| any `X-ClickHouse-Format` but `JSONCompact` | `rows` / `fields` | One synthetic row `{ __text: "<raw body>" }` under the single column `__text` ([§3.4](#34-default_formatjsoncompact-as-a-url-parameter-never-appended-to-the-sql)) |
| `meta` array | `columnTypes` | The declared type per column, keyed by its name in `fields` and spelled exactly as ClickHouse spells it — `Nullable(String)`, `LowCardinality(String)`, `Enum8('x' = 1)` — because the wrapper is what tells the user the column is nullable or low-cardinality. **Absent** when the envelope described no columns: a write, a format the user chose, or a statement with no result set. For a computed column such as `count()` this is the only source of a type at all, since no catalog entry exists for it (issue #273) |

Every name in `fields` is non-empty and appears once, and every column keeps its own value, measured on 26.7.1.1315.
An empty quoted alias is a syntax error, so no column comes back unnamed.
Two expressions under one alias (`SELECT 1 AS a, 2 AS a`) are refused with `MULTIPLE_EXPRESSIONS_FOR_ALIAS`, and a join qualifies a repeated column itself (`id`, `t2.id`).
But the server can still declare two `meta` columns of one name, in two ways.
`SELECT number, number FROM numbers(2)` and `SELECT rand(), rand()` repeat one expression, which ClickHouse evaluates once, so both columns carry the same value.
The qualified name can also collide with a name the statement already has: ``SELECT a.x, b.x, 3 AS `b.x` FROM (SELECT 1 AS x) a, (SELECT 2 AS x) b`` declares `x`, `b.x`, `b.x` with the values `1`, `2`, `3`.
The same holds for ``SELECT * FROM (SELECT 1 AS x) a JOIN (SELECT 2 AS x, 7 AS `b.x`) b ON 1=1`` (`1`, `2`, `7`), and `b.x` is also how a Nested sub-column is named.
Rows are read by position ([§3.4](#34-default_formatjsoncompact-as-a-url-parameter-never-appended-to-the-sql)), so both values survive, and the repeat is numbered as every provider numbers it: `fields` is `["x", "b.x", "b.x (2)"]`, the row is `{ x: 1, "b.x": 2, "b.x (2)": 3 }`, and `columnTypes` is keyed by the numbered names.

### 5.3 EXPLAIN

The EXPLAIN button is available (`supportsExplain: true`) and renders the plan tree described in
Expand Down
7 changes: 6 additions & 1 deletion docs/providers/couchbase.md
Original file line number Diff line number Diff line change
Expand Up @@ -562,11 +562,16 @@ operator — and a statement ending in a `#` run is returned unbounded rather th
| Source field | `QueryResult` field | Notes |
|--------------|---------------------|-------|
| result rows | `rows` | JSON objects exactly as the cluster returned them, except that an integer past 2^53 arrives as its exact digits (see below) |
| signature | `fields` | `null` for a wildcard signature, in which case columns are the union of the keys the rows carry, first seen first |
| signature | `fields` | `null` for a wildcard signature, in which case columns are the union of the keys the rows carry, first seen first; an empty key, from either source, is named `(No column name)` and the rows carrying it are keyed under that name (see below) |
| — | `rowCount` | `rows.length`, or the mutation count when a statement returned no rows |
| metrics `executionTime` | `executionTime` | The cluster's own time (excludes network latency); falls back to the measured wall clock when the cluster reported none |
| `warnings` | `warnings` | The notices the cluster attached to a statement it completed, each carrying its message and the cluster's own code **when it reported one** — an entry with no code arrives without one rather than with a substituted `0`, which is itself a legal code. **Absent** when the cluster reported no warnings at all — never an empty array, so the result UI decides from the field's presence alone (issue #273) |

**An empty key is a column named `(No column name)`.**
JSON carries an empty key, and the transport's `JSON.parse` keeps it, so a document `{"": 1}` or a signature `{"": "number"}` would otherwise answer a column `""`, which the grid cannot take.
[`uniquelyKeyedRows`](../../src/lib/db/utils/result-fields.ts) names it through `uniqueFieldNames`, numbered past any key the rows already use, and keys every row carrying it under that name.
Not measured on a live cluster; the rule is pinned by the provider's integration tests.

**An integer past 2^53 is handed over as its digits.** The query service sends a document's number
as the **unquoted** literal it was stored as, and `JSON.parse` rounds one past 2^53 with no error:
measured on 8.0.2 CE on 2026-10-04, a document `{"big":9007199254740993}` read through
Expand Down
7 changes: 5 additions & 2 deletions docs/providers/db2.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ Reading them is exact since 1.0.25: `SELECT *` over `APP.ALLTYPES` answers all 2
These were measured on 1.0.24, reported upstream one issue each, and fixed in [1.0.25](https://github.com/gurungabit/db2-node/releases/tag/v1.0.25); each now probes `GONE` on 12.1.0.0 and 11.5.9.0, and the provider workaround each one needed is gone with it unless the line says otherwise.

- K4: a LOB or XML column read beside other columns came back wrong, lost rows or failed: a BLOB beside a CLOB answered the CLOB's bytes, a CLOB(1M) beside a GRAPHIC dropped rows when a later LOB was NULL, a CLOB beside a DOUBLE failed with "Protocol error: Invalid SQLDTAGRP indicator 0xEF", and `SELECT *` over `APP.ALLTYPES` answered 1 row of 3 and 23 of 25 columns ([gurungabit/db2-node#19](https://github.com/gurungabit/db2-node/issues/19), [#20](https://github.com/gurungabit/db2-node/issues/20), fixed by [#27](https://github.com/gurungabit/db2-node/pull/27)). Every such read, NULL LOB rows and a 50000-byte CLOB included, now answers what each column read alone answers. The integrity warning is gone; the preview still leaves CLOB, DBCLOB and BLOB out, for K24, and reads XML.
- K15: two columns of one name collapsed into one value in a row ([gurungabit/db2-node#21](https://github.com/gurungabit/db2-node/issues/21), fixed by [#28](https://github.com/gurungabit/db2-node/pull/28)). Object rows still keep only the last; the provider reads `rowMode: "array"`, which keeps both, and names the repeat `A (2)`, as the Druid and Trino providers do.
- K15: two columns of one name collapsed into one value in a row ([gurungabit/db2-node#21](https://github.com/gurungabit/db2-node/issues/21), fixed by [#28](https://github.com/gurungabit/db2-node/pull/28)). Object rows still keep only the last; the provider reads `rowMode: "array"`, which keeps both, and names the repeat `A (2)`, never a name the statement itself declares.
- K16: a BOOLEAN parameter had to be a JS boolean, so the text `true` the grid's inline editor binds was refused ([gurungabit/db2-node#22](https://github.com/gurungabit/db2-node/issues/22), fixed by [#28](https://github.com/gurungabit/db2-node/pull/28)). The texts `true` and `false` are now stored as those booleans, so a BOOLEAN cell takes an inline edit.
- K17: a failure the driver raised itself carried no SQLSTATE and nothing else to classify it by ([gurungabit/db2-node#23](https://github.com/gurungabit/db2-node/issues/23), fixed by [#28](https://github.com/gurungabit/db2-node/pull/28)). It still carries no SQLSTATE, which only a server gives, and now carries a `driverCode`, which the provider maps (section 7.1).
- K23: a password holding `!`, `^`, `[`, `]` or `|` was sent wrongly and refused as "user id or password invalid" ([gurungabit/db2-node#25](https://github.com/gurungabit/db2-node/issues/25), fixed by [#29](https://github.com/gurungabit/db2-node/pull/29)). Measured with one user per character and one holding all five, over TLS under the default mechanism and under `userPassword` and without TLS under `userPassword`, through Bun and Node: every one signs in, the IBM CLP signs in with the same passwords over TCP, and a wrong password is still refused. The provider's refusal of those characters is gone.
Expand Down Expand Up @@ -321,7 +321,10 @@ A statement may start with a comment, which 1.0.22 refused (K18).
`fields` and `columnTypes` come from the driver's column metadata, so a zero-row result still has its header.
A SELECT reports the number of rows returned; any other statement reports the driver's affected-row count, which is 0 for an UPDATE or DELETE that matched no row (on 1.0.22 it was -2147221503, K19).
Each statement commits on its own.
The statement is read with `rowMode: "array"`, so two columns of one name keep both values: the repeat is named `A (2)`, then `A (3)`, skipping a name the statement already used, as the Druid and Trino providers name theirs (K15, fixed in 1.0.25).
The statement is read with `rowMode: "array"`, so two columns of one name keep both values: the repeat is named `A (2)`, then `A (3)`, never a name the statement itself declares, before or after the repeat (K15, fixed in 1.0.25).
So `SELECT 1 AS A, 2 AS A, 3 AS "A (2)"` comes back as `A`, `A (3)`, `A (2)`, and the user's own `A (2)` keeps its value.
Db2 names an unaliased expression itself (`1`, `2`, see below), so an empty name is not something it declares.
A row whose value count differs from the column count is refused with a query error rather than read by position, which would leave a value `undefined` or drop one.
The driver's diagnostics are passed through as result warnings, after the LOB warning of K24.
A failure the driver raises itself carries a `driverCode` and no SQLSTATE: `DB2_PARAMETER_COUNT` and `DB2_PARAMETER_TYPE`, a wrong number of parameters or a value that does not fit its target, are a query error, and `DB2_PROTOCOL` and `DB2_INVALID_OPTION` are the driver's own and stay a plain database error whatever their words say, each with the driver's message (K17, fixed in 1.0.25).
Every other error goes through the shared `mapDatabaseError()`, which reads the SQLSTATE and SQLCODE when the driver's message carries them.
Expand Down
Loading
Loading