Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
52 changes: 38 additions & 14 deletions src/drivers/rowset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,38 @@ export class SQLiteCloudRowset extends Array<SQLiteCloudRow> {
this.#metadata = metadata
this.#data = data

// adjust missing column names, duplicate column names, etc.
const columnNames = this.columnsNames
for (let i = 0; i < metadata.numberOfColumns; i++) {
if (!columnNames[i]) {
columnNames[i] = `column_${i}`
}
let j = 0
while (columnNames.findIndex((name, index) => index !== i && name === columnNames[i]) >= 0) {
columnNames[i] = `${columnNames[i]}_${j}`
j++
}
}
this.#columnsNames = SQLiteCloudRowset.getUniqueColumnsNames(this.originalColumnsNames, metadata.numberOfColumns)

for (let i = 0, start = 0; i < metadata.numberOfRows; i++, start += metadata.numberOfColumns) {
this[i] = new SQLiteCloudRow(this, columnNames, data.slice(start, start + metadata.numberOfColumns))
this[i] = new SQLiteCloudRow(this, this.#columnsNames, data.slice(start, start + metadata.numberOfColumns))
}
}

/**
* Adjust missing and duplicate column names so they can be used as row keys.
* Missing names become column_N, the first occurrence of a name is kept as is
* and later duplicates are suffixed with _1, _2, etc. skipping suffixes that
* are already used by another column, eg: id, id, id_1 becomes id, id_2, id_1
*/
private static getUniqueColumnsNames(originalNames: string[], numberOfColumns: number): string[] {
const names: string[] = []
for (let i = 0; i < numberOfColumns; i++) {
names.push(originalNames[i] || `column_${i}`)
}

const reserved = new Set(names)
const used = new Set<string>()
for (let i = 0; i < names.length; i++) {
if (used.has(names[i])) {
let j = 1
while (reserved.has(`${names[i]}_${j}`) || used.has(`${names[i]}_${j}`)) {
j++
}
names[i] = `${names[i]}_${j}`
}
used.add(names[i])
}
return names
}

/** Metadata contains number of rows and columns, column names, types, etc. */
Expand All @@ -71,6 +87,9 @@ export class SQLiteCloudRowset extends Array<SQLiteCloudRow> {
/** Actual data organized in rows */
#data: SQLiteCloudDataTypes[]

/** Unique column names used as keys in each row */
#columnsNames: string[]

/**
* Rowset version is 1 for a rowset with simple column names, 2 for extended metadata
* @see https://github.com/sqlitecloud/sdk/blob/master/PROTOCOL.md
Expand All @@ -89,8 +108,13 @@ export class SQLiteCloudRowset extends Array<SQLiteCloudRow> {
return this.#metadata.numberOfColumns
}

/** Array of columns names */
/** Array of unique columns names, the same names used as keys in each row (missing names are replaced, duplicates are suffixed) */
get columnsNames(): string[] {
return [...this.#columnsNames]
}

/** Array of columns names exactly as returned by the query, may contain duplicates */
get originalColumnsNames(): string[] {
return this.#metadata.columns.map(column => column.name)
}

Expand Down
73 changes: 73 additions & 0 deletions test/rowset.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,76 @@ describe('rowset', () => {
})
})
})

describe('rowset column names', () => {
function createRowset(names: (string | undefined)[], data: any[]): SQLiteCloudRowset {
return new SQLiteCloudRowset(
{
version: 1,
numberOfRows: data.length / names.length,
numberOfColumns: names.length,
columns: names.map(name => ({ name }) as any)
},
data
)
}

it('keeps the first duplicate column name and suffixes the following ones', () => {
const rowset = createRowset(['id', 'id', 'id'], [1, 2, 3])
expect(rowset.columnsNames).toEqual(['id', 'id_1', 'id_2'])
expect(rowset.originalColumnsNames).toEqual(['id', 'id', 'id'])
expect({ ...rowset[0] }).toEqual({ id: 1, id_1: 2, id_2: 3 })
expect(rowset[0].getData()).toEqual([1, 2, 3])
})

it('returns columnsNames matching the row keys', () => {
const rowset = createRowset(['id', 'name', 'id', 'customer_id', 'name'], [7, 'Ada', 42, 7, 'Laptop'])
expect(rowset.columnsNames).toEqual(['id', 'name', 'id_1', 'customer_id', 'name_1'])
expect(rowset.columnsNames.map(name => rowset[0][name])).toEqual([7, 'Ada', 42, 7, 'Laptop'])
})

it('skips suffixes already used by other columns', () => {
const rowset = createRowset(['id', 'id', 'id_1', 'id_2', 'id'], [1, 2, 3, 4, 5])
expect(rowset.columnsNames).toEqual(['id', 'id_3', 'id_1', 'id_2', 'id_4'])
expect({ ...rowset[0] }).toEqual({ id: 1, id_3: 2, id_1: 3, id_2: 4, id_4: 5 })
})

it('names missing columns by position', () => {
const rowset = createRowset(['', 'a', undefined, 'column_1'], [1, 2, 3, 4])
expect(rowset.columnsNames).toEqual(['column_0', 'a', 'column_2', 'column_1'])
expect(rowset.originalColumnsNames).toEqual(['', 'a', undefined, 'column_1'])
})

it('does not expose internal column names to mutation', () => {
const rowset = createRowset(['id', 'id'], [1, 2])
rowset.columnsNames.push('other')
expect(rowset.columnsNames).toEqual(['id', 'id_1'])
})

it('keeps unique column names after slice and filter', () => {
const rowset = createRowset(['id', 'id'], [1, 2, 3, 4, 5, 6])
const sliced = rowset.slice(1) as SQLiteCloudRowset
expect(sliced.columnsNames).toEqual(['id', 'id_1'])
expect({ ...sliced[0] }).toEqual({ id: 3, id_1: 4 })
const filtered = rowset.filter(row => row.id === 5) as SQLiteCloudRowset
expect(filtered.columnsNames).toEqual(['id', 'id_1'])
expect({ ...filtered[0] }).toEqual({ id: 5, id_1: 6 })
})

it('keeps the first duplicate column name for joined tables', done => {
const connection = getChinookTlsConnection()
connection.sendCommands(
'SELECT a.AlbumId, a.Title, t.TrackId AS AlbumId, t.Name AS Title FROM albums a JOIN tracks t ON t.AlbumId = a.AlbumId WHERE t.TrackId = 6;',
(error, rowset) => {
expect(error).toBeNull()
expect(rowset.originalColumnsNames).toEqual(['AlbumId', 'Title', 'AlbumId', 'Title'])
expect(rowset.columnsNames).toEqual(['AlbumId', 'Title', 'AlbumId_1', 'Title_1'])
expect({ ...rowset[0] }).toEqual({ AlbumId: 1, Title: 'For Those About To Rock We Salute You', AlbumId_1: 6, Title_1: 'Put The Finger On You' })
expect(rowset.columnsNames.map(name => rowset[0][name])).toEqual(rowset[0].getData())

done()
connection.close()
}
)
})
})
Loading