From 1f4426bf2b2b74f216c77590cc84cf78a8941d51 Mon Sep 17 00:00:00 2001 From: Iheanyi Ekechukwu Date: Mon, 3 Oct 2022 11:58:57 -0500 Subject: [PATCH 1/6] Allow returning rows as an array or object. --- __tests__/index.test.ts | 34 +++++++++++++++++++++++++++ src/index.ts | 52 +++++++++++++++++++++++++++++++++-------- 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index e9063d4..aa81a53 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -163,6 +163,40 @@ describe('execute', () => { expect(got2).toEqual(want) }) + test('it properly returns and decodes a select query with rows as array when designated', async () => { + const mockResponse = { + session: mockSession, + result: { + fields: [{ name: ':vtg1', type: 'INT32' }], + rows: [{ lengths: ['1'], values: 'MQ==' }] + } + } + + const want: ExecutedQuery = { + headers: [':vtg1'], + types: { ':vtg1': 'INT32' }, + rows: [[1]], + size: 1, + statement: 'SELECT 1 from dual;', + time: 1, + rowsAffected: null, + insertId: null + } + + mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts) => { + expect(opts.headers['authorization']).toMatch(/Basic /) + const bodyObj = JSON.parse(opts.body.toString()) + expect(bodyObj.session).toEqual(null) + return mockResponse + }) + + const connection = connect(config) + const got = await connection.execute('SELECT 1 from dual;', null, { as: 'array' }) + got.time = 1 + + expect(got).toEqual(want) + }) + test('it properly returns an executed query for a DDL statement', async () => { const mockResponse = { session: mockSession, diff --git a/src/index.ts b/src/index.ts index c1048a9..31c4c05 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ export { hex } from './text.js' import { decode } from './text.js' import { Version } from './version.js' -type Row = Record +type Row = Record | any[] interface VitessError { message: string @@ -97,6 +97,18 @@ interface QueryResult { rows?: QueryResultRow[] } +type ExecuteAs = 'array' | 'object' + +type ExecuteOptions = { + as: ExecuteAs +} + +type ExecuteArgs = object | Array | null + +const defaultExecuteOptions: ExecuteOptions = { + as: 'object' +} + export class Client { private config: Config @@ -108,8 +120,12 @@ export class Client { return this.connection().transaction(fn) } - async execute(query: string, args?: object | any[]): Promise { - return this.connection().execute(query, args) + async execute( + query: string, + args: ExecuteArgs = null, + options: ExecuteOptions = defaultExecuteOptions + ): Promise { + return this.connection().execute(query, args, options) } connection(): Connection { @@ -126,8 +142,12 @@ class Tx { this.conn = conn } - async execute(query: string, args?: object | any[]): Promise { - return this.conn.execute(query, args) + async execute( + query: string, + args: ExecuteArgs = null, + options: ExecuteOptions = defaultExecuteOptions + ): Promise { + return this.conn.execute(query, args, options) } } @@ -171,7 +191,11 @@ export class Connection { await this.createSession() } - async execute(query: string, args?: any): Promise { + async execute( + query: string, + args: ExecuteArgs = null, + options: ExecuteOptions = defaultExecuteOptions + ): Promise { const url = new URL('/psdb.v1alpha1.Database/Execute', `https://${this.config.host}`) const formatter = this.config.format || format @@ -191,7 +215,7 @@ export class Connection { this.session = session - const rows = result ? parse(result, this.config.cast || cast) : [] + const rows = result ? parse(result, this.config.cast || cast, options.as) : [] const headers = result ? result.fields?.map((f) => f.name) ?? [] : [] const typeByName = (acc, { name, type }) => ({ ...acc, [name]: type }) @@ -251,18 +275,26 @@ export function connect(config: Config): Connection { return new Connection(config) } -function parseRow(fields: Field[], rawRow: QueryResultRow, cast: Cast): Row { +function parseRow(fields: Field[], rawRow: QueryResultRow, cast: Cast, returnAs: ExecuteAs): Row { const row = decodeRow(rawRow) + + if (returnAs === 'array') { + return fields.reduce((acc, field, ix) => { + acc.push(cast(field, row[ix])) + return acc + }, [] as Row) + } + return fields.reduce((acc, field, ix) => { acc[field.name] = cast(field, row[ix]) return acc }, {} as Row) } -function parse(result: QueryResult, cast: Cast): Row[] { +function parse(result: QueryResult, cast: Cast, returnAs: ExecuteAs): Row[] { const fields = result.fields const rows = result.rows ?? [] - return rows.map((row) => parseRow(fields, row, cast)) + return rows.map((row) => parseRow(fields, row, cast, returnAs)) } function decodeRow(row: QueryResultRow): Array { From 3d582e7e376d85f89fa8955ee522150917969d9e Mon Sep 17 00:00:00 2001 From: Iheanyi Ekechukwu Date: Mon, 3 Oct 2022 12:12:53 -0500 Subject: [PATCH 2/6] Return fields back with executed queries. --- __tests__/index.test.ts | 9 +++++++++ src/index.ts | 7 +++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index aa81a53..eba7e7b 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -129,6 +129,7 @@ describe('execute', () => { const want: ExecutedQuery = { headers: [':vtg1'], types: { ':vtg1': 'INT32' }, + fields: [{ name: ':vtg1', type: 'INT32' }], rows: [{ ':vtg1': 1 }], size: 1, statement: 'SELECT 1 from dual;', @@ -176,6 +177,7 @@ describe('execute', () => { headers: [':vtg1'], types: { ':vtg1': 'INT32' }, rows: [[1]], + fields: [{ name: ':vtg1', type: 'INT32' }], size: 1, statement: 'SELECT 1 from dual;', time: 1, @@ -209,6 +211,7 @@ describe('execute', () => { const want: ExecutedQuery = { headers: [], types: {}, + fields: [], rows: [], rowsAffected: null, insertId: null, @@ -238,6 +241,7 @@ describe('execute', () => { const want: ExecutedQuery = { headers: [], types: {}, + fields: [], rows: [], rowsAffected: 1, insertId: null, @@ -268,6 +272,7 @@ describe('execute', () => { const want: ExecutedQuery = { headers: [], types: {}, + fields: [], rows: [], rowsAffected: 1, insertId: '2', @@ -352,6 +357,7 @@ describe('execute', () => { headers: [':vtg1'], rows: [{ ':vtg1': 1 }], types: { ':vtg1': 'INT32' }, + fields: [{ name: ':vtg1', type: 'INT32' }], size: 1, insertId: null, rowsAffected: null, @@ -384,6 +390,7 @@ describe('execute', () => { const want: ExecutedQuery = { headers: [':vtg1'], types: { ':vtg1': 'INT32' }, + fields: [{ name: ':vtg1', type: 'INT32' }], rows: [{ ':vtg1': 1 }], size: 1, insertId: null, @@ -417,6 +424,7 @@ describe('execute', () => { const want: ExecutedQuery = { headers: [':vtg1'], types: { ':vtg1': 'INT64' }, + fields: [{ name: ':vtg1', type: 'INT64' }], rows: [{ ':vtg1': BigInt(1) }], size: 1, insertId: null, @@ -453,6 +461,7 @@ describe('execute', () => { const want: ExecutedQuery = { headers: ['document'], types: { document: 'JSON' }, + fields: [{ name: 'document', type: 'JSON' }], rows: [{ document: JSON.parse(document) }], size: 1, insertId: null, diff --git a/src/index.ts b/src/index.ts index 31c4c05..c37cdb2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,7 @@ export interface ExecutedQuery { headers: string[] types: Types rows: Row[] + fields: Field[] size: number statement: string insertId: string | null @@ -215,15 +216,17 @@ export class Connection { this.session = session + const fields = result?.fields ?? [] const rows = result ? parse(result, this.config.cast || cast, options.as) : [] - const headers = result ? result.fields?.map((f) => f.name) ?? [] : [] + const headers = fields.map((f) => f.name) const typeByName = (acc, { name, type }) => ({ ...acc, [name]: type }) - const types = result ? result.fields?.reduce(typeByName, {}) ?? {} : {} + const types = fields.reduce(typeByName, {}) return { headers, types, + fields, rows, rowsAffected, insertId, From 6450a6f9dd6532f6e107913d006b0e9a8103cd09 Mon Sep 17 00:00:00 2001 From: Iheanyi Ekechukwu Date: Mon, 3 Oct 2022 12:16:08 -0500 Subject: [PATCH 3/6] Make as option optional. --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index c37cdb2..a8a044c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -101,7 +101,7 @@ interface QueryResult { type ExecuteAs = 'array' | 'object' type ExecuteOptions = { - as: ExecuteAs + as?: ExecuteAs } type ExecuteArgs = object | Array | null @@ -217,7 +217,7 @@ export class Connection { this.session = session const fields = result?.fields ?? [] - const rows = result ? parse(result, this.config.cast || cast, options.as) : [] + const rows = result ? parse(result, this.config.cast || cast, options.as || 'object') : [] const headers = fields.map((f) => f.name) const typeByName = (acc, { name, type }) => ({ ...acc, [name]: type }) From c7f43abfd2f78ea483284bd5b3f4d36948abd969 Mon Sep 17 00:00:00 2001 From: Iheanyi Ekechukwu Date: Mon, 3 Oct 2022 12:20:00 -0500 Subject: [PATCH 4/6] Use any array. --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index a8a044c..26fa4ef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -104,7 +104,7 @@ type ExecuteOptions = { as?: ExecuteAs } -type ExecuteArgs = object | Array | null +type ExecuteArgs = object | any[] | null const defaultExecuteOptions: ExecuteOptions = { as: 'object' From bedd2672e8f8f4e09e2de65b4cf7b86238957d9b Mon Sep 17 00:00:00 2001 From: Iheanyi Ekechukwu Date: Mon, 3 Oct 2022 12:26:16 -0500 Subject: [PATCH 5/6] Break row parsing into two separate functions based on return type. --- src/index.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/index.ts b/src/index.ts index 26fa4ef..8bcd8bc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -278,15 +278,15 @@ export function connect(config: Config): Connection { return new Connection(config) } -function parseRow(fields: Field[], rawRow: QueryResultRow, cast: Cast, returnAs: ExecuteAs): Row { +function parseArrayRow(fields: Field[], rawRow: QueryResultRow, cast: Cast): Row { const row = decodeRow(rawRow) + return fields.map((field, ix) => { + return cast(field, row[ix]) + }) +} - if (returnAs === 'array') { - return fields.reduce((acc, field, ix) => { - acc.push(cast(field, row[ix])) - return acc - }, [] as Row) - } +function parseObjectRow(fields: Field[], rawRow: QueryResultRow, cast: Cast): Row { + const row = decodeRow(rawRow) return fields.reduce((acc, field, ix) => { acc[field.name] = cast(field, row[ix]) @@ -297,7 +297,9 @@ function parseRow(fields: Field[], rawRow: QueryResultRow, cast: Cast, returnAs: function parse(result: QueryResult, cast: Cast, returnAs: ExecuteAs): Row[] { const fields = result.fields const rows = result.rows ?? [] - return rows.map((row) => parseRow(fields, row, cast, returnAs)) + return rows.map((row) => + returnAs === 'array' ? parseArrayRow(fields, row, cast) : parseObjectRow(fields, row, cast) + ) } function decodeRow(row: QueryResultRow): Array { From 382a1c4a735e0bd8e19614165c72b4dafea545b5 Mon Sep 17 00:00:00 2001 From: Iheanyi Ekechukwu Date: Mon, 3 Oct 2022 12:26:38 -0500 Subject: [PATCH 6/6] Whitespace. --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 8bcd8bc..125b3bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -280,6 +280,7 @@ export function connect(config: Config): Connection { function parseArrayRow(fields: Field[], rawRow: QueryResultRow, cast: Cast): Row { const row = decodeRow(rawRow) + return fields.map((field, ix) => { return cast(field, row[ix]) })