Repository navigation
fix(db): give every result column a name and a value of its own - #1575
Merged
Merged
Conversation
…eps its value Add uniqueFieldNames beside unionFields: a column a driver declares with no name is named "(No column name)", and a repeated name is numbered "name (2)", never taking a name the result itself declares. QueryResult.fields now states the invariant the providers keep: non-empty, unique, and the keys rows use.
…lumns keep their values query(), queryReadOnly() and queryInTransaction() now ask the driver for array row mode on the request that runs the user's statement, name the columns with uniqueFieldNames, and key rows, columnTypes and the zoneless value conversion by those names. SELECT @@Version answered fields [""], which crashed the results grid, and SELECT 1 AS a, 2 AS a merged both values into one column. FOR JSON and FOR XML stay one column holding the whole text, a row whose value count differs from the column count is refused, and the provider's own catalog and admission reads keep object rows. The transaction path now names a zero-row result's columns, which closes the mssql bullet of BACKLOG X9.
…le stale test names The batch test now gives its second set a datetime2 column and checks the converted text under its unique name, so dropping that conversion fails. Two tests are retitled to what they now exercise, a test comment no longer cites the deleted X9 bullet, and the columnTypes section names all three execution paths.
…column keeps its value pg and mysql2 object rows keep only the last of two columns that share a name, so a join projecting id from both tables showed one table's id under both headers. query(), queryInTransaction() and the PostgreSQL read-only profile now ask for array rows (rowMode "array", rowsAsArray), name the columns with uniqueFieldNames and key each row by position; columnTypes follows the same names, and a row whose value count differs from the column count raises. A multi-statement PostgreSQL text and a MySQL CALL answer a list of results: rows, fields and columnTypes are now the first result set's and resultSets lists every set. Before, the PostgreSQL text answered no rows and a CALL threw a TypeError after the procedure ran. Measured on PostgreSQL 16 and MySQL 8.4; provider docs updated. object-route.ts citations of postgres.ts line numbers moved with the inserted helpers.
… docs MySQL answers an empty column name for SELECT '' and SELECT 1 AS ''; the mysql doc claimed it never does. State the measured before and after, and pin SELECT '', '' in a mock and a wire test. Word the PostgreSQL multi-result rule by what the code tests (a statement that answers no column), and stop claiming mysql2 lets a __proto__ column through.
…mn name keeps its value The provider keyed each row by column name and took fields from the first row's keys, so SELECT 1 AS a, 2 AS a answered one column holding 2 and SELECT 1 AS "" answered a column named "". Both drivers now read user results as arrays (bun:sqlite values(), node:sqlite setReturnArrays) and key them by uniqueFieldNames, on query() and queryReadOnly(), with each declared type paired to its column by position. An empty result now names its columns. keyRowsByPosition is the shared row builder, and refuses a row whose value count differs from the column count.
…its value getRowObjectsJson() keys a repeated column a:1 while columnNames() lists the name twice, so both headers showed the first value, and a column the statement itself named a:1 was overwritten by the repeat. The client now reads getRowsJson() and keys each row by position under uniqueFieldNames, so query() and queryReadOnly() answer a, a (2) with both values and both types. DuckDB refuses an empty alias at parse time, pinned as such.
…lumn keeps its value The Hrana transport keyed each row by the declared name, so a repeated name kept only the last value while fields listed it twice, and an unnamed column became column_N, which a column the statement named column_1 overwrote. Columns are now named with uniqueFieldNames and rows keyed by position, with declared types under the same names. A row whose value count is not the column count, or that is not a list, is refused instead of padded.
… one name cassandra-driver builds each row as row[column.name] = value, so two columns of one name reach the provider holding only the last value, and both grid headers showed it. That result is now refused with a query error naming the column and asking for an alias, since the earlier value cannot be recovered. A column declared with an empty name is keyed "(No column name)", with its declared type under the same name.
…rch, Db2 and Trino The four private copies of the repeat-numbering rule never reserved a name the result declares later, so `SELECT 1 AS a, 2 AS a, 3 AS "a (2)"` showed the user's own `a (2)` column under a generated name. Each transport now calls uniqueFieldNames, which numbers that repeat `a (3)` and names an empty Trino declaration `(No column name)`. Docs state the new numbering.
…ruid, search, Db2 and Trino Each transport read a row by position and padded a short one with null or dropped the tail of a long one, so a malformed answer reached the grid with invented or missing values. A count mismatch, or a row that is not an array, now raises the provider's own error.
…olumn A declaration with no name member was keyed "undefined"; it now reaches the shared helper as an empty name and shows as (No column name), as Trino already does.
… as one The panel's one ChunkBoundary caught every render error, called each a chunk that did not load, and never reset, so one result the grid could not draw (a SQL Server column with no name) left the Reload notice over every later result, mode and tab. lazyRetry and GraphView now throw a ChunkLoadError for a load that failed, and only that gets the Reload copy; any other error says the view could not be displayed, with its message when it has one, and offers Try again. The boundary resets when its resetKeys change, and BottomPanel keys it by mode, result, agent artifact and explain plan.
Inline edit writes the field name into UPDATE ... SET, and a generated name such as `name (2)` or `(No column name)` is no column of the table: PostgreSQL 16 refuses `SET "name (2)"` on customers(id, name) as a missing column, and a table that had a column of that name would take the write. Such a cell now opens no editor and says why.
…umn it repeats A result names a repeated column 'email (2)', so the second email no longer shares the first's key and its own value reached the grid, exports, profiler and graph unmasked. Detection now also matches the base of a numbered name.
loadCytoscape now tags a rejected import as ChunkLoadError, so a failure after both packages arrived shows as a render error instead of offering Reload.
…lumn MongoDB and Couchbase took their columns from the keys the rows carry, and a key "" became an empty field, which breaks the QueryResult.fields invariant. uniquelyKeyedRows names it through uniqueFieldNames and keys the rows carrying it under that name; rows without one are answered as read.
…roducer The grid's inline-edit refusal and masking each parsed "name (N)" with their own regex. numberedRepeatBase in result-fields.ts is now the one reader, and both callers are pinned against uniqueFieldNames output, so a format change fails their tests too. Each caller keeps what it matched before.
result-fields.ts no longer claims to be server only or to copy the transports that now call it; column-types.ts says the names arrive unique and counts its callers by provider; types.ts names the three ways providers keep names apart. cassandra.md says why a repeat of one column is refused too.
The three providers each re-wrote the length check and positional keying the helper already does, in three wordings and without the statement text. They now call keyRowsByPosition, so a row of the wrong width raises one QueryError that carries the statement. mssql keeps its own reader for the FOR JSON case.
The panel's boundary printed the thrown message under the notice. A message can quote a value (a JSON.parse SyntaxError quotes its input), which would then show past masking. The notice now says a fixed sentence and the message goes only to the log.
…uildRows Both line numbers drifted when the search transport moved to uniqueFieldNames. The OpenSearch paragraphs this branch rewrote cite no transport line.
…hared helper ClickHouse answers SELECT number, number FROM numbers(2) with two meta columns of one name and one value (measured on 26.7.1). Key fields, rows and columnTypes by uniqueFieldNames so the grid never gets two columns of one id.
InfluxDB 3.12 Core answers SELECT c1.usage, c2.usage FROM cpu c1 CROSS
JOIN cpu c2 with the line {"usage":1.5,"usage":1.5}. JSON.parse keeps
one value and a line omits the key of a null cell, so the result lost a
column silently. Refuse it with a sentence that names the column and asks
for an alias.
…eps its value ClickHouse can declare two columns of one name with different values: a join qualifies a column as b.x, which can collide with a column the statement already calls b.x. Under default_format=JSON the row object named the key twice and JSON.parse kept the last value, so the previous fix showed 3 under both headers and lost 2 (measured on 26.7.1.1315). Ask for JSONCompact, number repeated names with uniqueFieldNames in the transport and build each row by position. A row that is not an array or whose value count differs from meta is refused with an explicit error. An explicit FORMAT JSON in the user's SQL now comes back as raw text, like any other format the user chose.
…cument the all-null case A three-way self join names a key three times, so the refusal now says "more than one column named host" instead of "two columns". The doc adds the measured limit: a repeated name whose other column is null on every line never appears twice on a line, so it comes back as one column.
…uniquelyKeyedRows copies
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…test can delete it Under Bun the node driver is Bun's own node:sqlite, whose close() leaves its statements holding the database, WAL and shared-memory file until they are collected (three descriptors after disconnect, none after a collection, measured through /proc/self/fd). On windows-latest the fixture's removal failed with EBUSY. Node's own close() releases them.
Neo4j 5.26 refuses two output columns of one name but answers an empty one for RETURN 1 AS ``, which reached the grid as an empty column id. The Bolt client now names the record keys through uniqueFieldNames; values were already read by position.
InfluxDB 3.12 Core answers {"":1} for SELECT 1 AS "", and the jsonl reader passed the empty key to the grid as a field. The ordered key union now goes through uniqueFieldNames and each row is keyed by position; the repeated-key refusal is unchanged.
The loop counted up from 2 for every repeat of a name, so n repeats checked about n squared over two candidates, and its condition tested a name while its update moved a counter (SonarCloud S1994). A next number is now kept per base; every result is unchanged (the existing tests pass, and 300,000 random inputs matched the old loop).
…on changes The docs and schema diff views draw the schema and the connection, which were not among the boundary's reset keys, so a schema one of them could not draw kept the notice up through a schema refresh or a connection switch.
positionalResultSet and rowValues raised QueryError without the statement, unlike keyRowsByPosition and the other providers, so the error payload of a refused result set named no query. Every caller now passes its statement through.
describeColumns passed a non-string column name through String(), so an object would have reached the grid as "[object Object]" (SonarCloud S6551). An absent name is still an unnamed column; a name of any other type is now an explicit engine transport error.
… catch names The numberedRepeatBase import sits with the file's header, sameKeys compares lengths through an optional chain, and the two catch parameters take the error naming the repository's own catch clauses use (error, and retryError beside an outer error).
…ike to its own The negative case asserted only names no default pattern matches, so it passed without the numbered-repeat reading. It now checks that email (2) takes the email rule while email (work) keeps its own pattern and email (2)x is not read back to email; the old matcher fails it.
…s measured node-oracledb 6.10 numbers a repeated column in metaData before any row is keyed (A, A_1; A, A_2, A_1 beside a declared A_1), in both out formats, and the server refuses an empty alias with ORA-01741. Measured against Oracle XE on 2026-10-07; the integration test pins the provider passing the driver's names and values through.
… NAME (2) node-oracledb renames a repeated column in metaData (A, A_1, skipping a declared A_1), but it hands each column to the call's fetchTypeHandler under its declared name first. query() and queryInTransaction() now record those names in the handler, read array rows, and name the columns through uniqueFieldNames and keyRowsByPosition like the other SQL providers, so a repeat reads A (2) and masking and the inline-edit refusal treat it as one. A statement served from the statement cache arrives already renamed, so both paths run with keepInStmtCache false. Measured on Oracle XE on 2026-10-07 through the provider, both paths, run twice on one connection.
An OpenSearch alias that is present but not a string was passed over for the column name, which is the name the user aliased away. It now raises the same engine error a non-text name does; a null alias is still no alias.
…review under DOC8
…epeat as NAME (2)" This reverts commit baa9d38.
…d names are not rebuilt The declared names do reach a fetchTypeHandler before node-oracledb renames a repeat, but only for a statement outside the statement cache; rebuilding the name (2) form would cost a cache miss on every editor statement, and a CURSOR column read as arrays loses its nested names. The rewrite that did it is reverted, and section 5.1 states what masking and the inline-edit refusal do with NAME_1.
cevheri
added a commit
that referenced
this pull request
Oct 7, 2026
… and the edit refusal (U95) Found by the external review of #1575: node-oracledb numbers a repeat NAME_1 and the provider keeps it, while masking and the inline-edit refusal read only the name (N) form.
cevheri
added a commit
that referenced
this pull request
Oct 7, 2026
…nly its first (U96) Found by the external review of #1575: an EXEC of a procedure that selects twice, a batch inside a transaction, and a MySQL CALL once its sets are read show the first set with no sign of the others.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Two defects from the browser pass of #1570 had one cause: providers keyed result rows by the column names the driver declared.
SELECT COUNT(*) FROM t). The grid threw on the empty name, and the results panel stayed broken until a reload.idfrom both sides of a join, lost a value silently on PostgreSQL, MySQL, SQLite, DuckDB, libSQL, Cassandra, ClickHouse and InfluxDB 3: both headers showed one table's value.Providers now read results by position and name them with one rule,
uniqueFieldNames: an unnamed column or an empty document key is(No column name), a repeat isname (2).QueryResult.fieldsstates the invariant. Where the driver has already merged a repeat (Cassandra, InfluxDB 3), the result is refused with a clear error. The results panel resets when its result changes, masking covers a numbered repeat, and inline edit refuses a generated name.Visible change: a ClickHouse statement with its own
FORMAT JSONnow shows raw text, like any other explicit format.Verified: the full gate set with 100% line coverage, and in a browser on PostgreSQL 16, MySQL 8.4, SQL Server 2022 and SQLite, agent mode included.