A table can't have two columns with the same name, but a query result can — SELECT * over a join is the everyday case:
SELECT a.id, b.id FROM a JOIN b ON b.a_id = a.id; -- id, id
SELECT * FROM orders o JOIN customers c ON …; -- id, id, name, name, …
SELECT count(*), count(*) FROM t; -- count(*), count(*)
An object can't hold two id keys, so SQLiteCloudRowset renames duplicates when building each SQLiteCloudRow (lib/drivers/rowset.js, constructor). Two bugs in that renaming:
1. The first duplicate is renamed, the last keeps the clean name — backwards from what callers expect.
Reproduces with no connection:
const { SQLiteCloudRowset } = require("@sqlitecloud/drivers");
const rowset = new SQLiteCloudRowset(
{
version: 1,
numberOfRows: 1,
numberOfColumns: 3,
columns: [{ name: "id" }, { name: "id" }, { name: "id" }],
},
[1, 2, 3],
);
console.log(rowset[0]); // { id_0: 1, id_0_1: 2, id: 3 }
console.log(rowset.columnsNames); // [ 'id', 'id', 'id' ]
console.log(rowset[0].getData()); // [ 1, 2, 3 ]
Expected for SELECT a.id, b.id (a.id=7, b.id=42): { id: 7, id_1: 42 }. Actual: { id_0: 7, id: 42 }.
The rename loop walks columns left to right and renames the current column if any other column shares its name. The first id sees a duplicate later in the list and gets renamed; by the time the loop reaches the last id, the earlier ones have already been renamed away, so it looks unique and stays clean. Code that reads row.id almost always means the first id in the select list — the driver hands back the last one instead.
2. columnsNames doesn't match the renamed row keys.
The constructor renames a local copy of the names, but the columnsNames getter maps over metadata.columns fresh on every call — it never sees the rename. So:
- rows have the keys
id_0, id_0_1, id (see repro above)
rowset.columnsNames returns ["id", "id", "id"]
Any consumer that builds headers from columnsNames and reads cells with row[name] reads row["id"] for every duplicate column and silently gets the same (last) value in every cell — no error, no empty cell, nothing to suggest the data is wrong.
Impact example: customers(id=7, name="Ada") joined with orders(id=42, customer_id=7, name="Laptop"):
SELECT * FROM customers c JOIN orders o ON o.customer_id = c.id;
A naive consumer using columnsNames + row[name] renders id=42, name="Laptop" for both the customer and order columns — the customer's own id/name are silently replaced by the order's, with correct-looking headers, column count, and row count.
Requested fix:
- Suffix the later duplicates, keep the first occurrence's name clean:
id, id_1, id_2. Skip any suffix already used by a real column.
- Make
columnsNames return the same names the rows are actually keyed by, so the two can be used together.
- Expose the original, un-renamed names separately (e.g.
originalColumnsNames) for consumers that want to show exactly what the query returned.
Note: fixing #1 changes the keys of any row that contains duplicate names today, so it's a breaking change for anyone relying on the current id_0/id_0_1 scheme — worth a changelog note, though those keys seem unlikely to be depended on deliberately.
Tested against @sqlitecloud/drivers 1.0.878.
A table can't have two columns with the same name, but a query result can —
SELECT *over a join is the everyday case:An object can't hold two
idkeys, soSQLiteCloudRowsetrenames duplicates when building eachSQLiteCloudRow(lib/drivers/rowset.js, constructor). Two bugs in that renaming:1. The first duplicate is renamed, the last keeps the clean name — backwards from what callers expect.
Reproduces with no connection:
Expected for
SELECT a.id, b.id(a.id=7,b.id=42):{ id: 7, id_1: 42 }. Actual:{ id_0: 7, id: 42 }.The rename loop walks columns left to right and renames the current column if any other column shares its name. The first
idsees a duplicate later in the list and gets renamed; by the time the loop reaches the lastid, the earlier ones have already been renamed away, so it looks unique and stays clean. Code that readsrow.idalmost always means the firstidin the select list — the driver hands back the last one instead.2.
columnsNamesdoesn't match the renamed row keys.The constructor renames a local copy of the names, but the
columnsNamesgetter maps overmetadata.columnsfresh on every call — it never sees the rename. So:id_0,id_0_1,id(see repro above)rowset.columnsNamesreturns["id", "id", "id"]Any consumer that builds headers from
columnsNamesand reads cells withrow[name]readsrow["id"]for every duplicate column and silently gets the same (last) value in every cell — no error, no empty cell, nothing to suggest the data is wrong.Impact example:
customers(id=7, name="Ada")joined withorders(id=42, customer_id=7, name="Laptop"):A naive consumer using
columnsNames+row[name]rendersid=42, name="Laptop"for both the customer and order columns — the customer's own id/name are silently replaced by the order's, with correct-looking headers, column count, and row count.Requested fix:
id,id_1,id_2. Skip any suffix already used by a real column.columnsNamesreturn the same names the rows are actually keyed by, so the two can be used together.originalColumnsNames) for consumers that want to show exactly what the query returned.Note: fixing #1 changes the keys of any row that contains duplicate names today, so it's a breaking change for anyone relying on the current
id_0/id_0_1scheme — worth a changelog note, though those keys seem unlikely to be depended on deliberately.Tested against
@sqlitecloud/drivers1.0.878.