From 7a7802a4b67817d138b4c10dcea7e4e63dd09dd9 Mon Sep 17 00:00:00 2001 From: Iheanyi Ekechukwu Date: Wed, 3 Aug 2022 11:58:22 -0500 Subject: [PATCH 01/12] Build before publishing using prepublishOnly --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 4f8740f..9cf8d33 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "build": "tsc", "lint": "eslint src/ __tests__/", "pretest": "npm run build", - "test": "jest" + "test": "jest", + "prepublishOnly": "npm run build" }, "repository": { "type": "git", From eb4a445c337c14f10dafabcc535563936b43721f Mon Sep 17 00:00:00 2001 From: Iheanyi Ekechukwu Date: Wed, 3 Aug 2022 11:59:14 -0500 Subject: [PATCH 02/12] Only export types that we are using, cleanup error type within ExecutedQuery. --- src/index.ts | 48 +++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/index.ts b/src/index.ts index d13cf16..6a01d3c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,22 @@ type ReqInit = Pick & { body: string } +type Row = Record + +interface VitessError { + message: string + code: string +} + +export interface ExecutedQuery { + headers: string[] + rows: Row[] + size: number + statement: string + error: VitessError | null + time: number +} + export interface Config { username: string password: string @@ -13,12 +29,12 @@ export interface Config { fetch?: (input: string, init?: ReqInit) => Promise> } -export interface QueryResultRow { +interface QueryResultRow { lengths: string[] values: string } -export interface QueryResultField { +interface QueryResultField { name?: string type?: string table?: string @@ -37,20 +53,15 @@ export interface QueryResultField { columnType?: string | null } -export type QuerySession = unknown +type QuerySession = unknown -interface VitessError { - message: string - code: string -} - -export interface QueryExecuteResponse { +interface QueryExecuteResponse { session: QuerySession result: QueryResult | null error?: VitessError } -export interface QueryResult { +interface QueryResult { rowsAffected?: number | null insertId?: number | null fields?: QueryResultField[] | null @@ -196,20 +207,11 @@ function parseColumn(type: string, value: string | null): number | string | null case 'UINT32': case 'UINT64': return parseInt(value, 10) + case 'FLOAT32': + case 'FLOAT64': + case 'DECIMAL': + return parseFloat(value) default: return utf8Encode(value) } } - -type Row = Record - -export interface ExecutedQuery { - headers?: string[] - rows?: Row[] - size?: number - statement?: string - rawError?: Error - errorCode?: string - errorMessage?: string - time?: number -} From b0e3f8fbb2a37d9ae50a4de8fcde518354621af2 Mon Sep 17 00:00:00 2001 From: Iheanyi Ekechukwu Date: Wed, 3 Aug 2022 12:00:24 -0500 Subject: [PATCH 03/12] Move connect function down. --- src/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 6a01d3c..490c26a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -84,10 +84,6 @@ export class Client { } } -export function connect(config: Config): Connection { - return new Connection(config) -} - export class Connection { private config: Config private session: QuerySession | null @@ -163,6 +159,10 @@ export class Connection { } } +export function connect(config: Config): Connection { + return new Connection(config) +} + function parseRow(fields: QueryResultField[], rawRow: QueryResultRow): Row { const row = decodeRow(rawRow) return fields.reduce((acc, field, ix) => { From 82d1d059c7f07dc6d2b3a69d1404ec4c21f63610 Mon Sep 17 00:00:00 2001 From: Iheanyi Ekechukwu Date: Wed, 3 Aug 2022 12:00:32 -0500 Subject: [PATCH 04/12] Make config private. --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 490c26a..6680ed8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -69,7 +69,7 @@ interface QueryResult { } export class Client { - config: Config + private config: Config constructor(config: Config) { this.config = config From 7ca186dd5018a8ef642888f96cc142cc57127c9c Mon Sep 17 00:00:00 2001 From: Iheanyi Ekechukwu Date: Wed, 3 Aug 2022 12:00:45 -0500 Subject: [PATCH 05/12] Inline await JSON. --- src/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 6680ed8..61e9b28 100644 --- a/src/index.ts +++ b/src/index.ts @@ -124,8 +124,7 @@ export class Connection { }) if (response.ok) { - const result = await response.json() - return result + return await response.json() } else { throw new Error(`${response.status} ${response.statusText}`) } From 769503fa8e820707c172fbc076789d7808b90ac5 Mon Sep 17 00:00:00 2001 From: Iheanyi Ekechukwu Date: Wed, 3 Aug 2022 12:00:53 -0500 Subject: [PATCH 06/12] Return error response from the executed query. --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 61e9b28..a8e969b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -141,7 +141,6 @@ export class Connection { const time = Date.now() - startTime const { result, session, error } = saved - if (error) throw new Error(error.message) this.session = session @@ -151,6 +150,7 @@ export class Connection { return { headers, rows, + error, size: rows.length, statement: query, time From 7ef47c06155653a9deb24e05fa115e9acc19f2cd Mon Sep 17 00:00:00 2001 From: Iheanyi Ekechukwu Date: Wed, 3 Aug 2022 12:05:28 -0500 Subject: [PATCH 07/12] Make error a maybe type, fix up tests. --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index a8e969b..e051c4d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,7 +18,7 @@ export interface ExecutedQuery { rows: Row[] size: number statement: string - error: VitessError | null + error?: VitessError | null time: number } From 5f8b721b14a8cbfbb6f06e538a30face501f044e Mon Sep 17 00:00:00 2001 From: Iheanyi Ekechukwu Date: Wed, 3 Aug 2022 12:16:01 -0500 Subject: [PATCH 08/12] Add tests for error case. --- __tests__/index.test.ts | 49 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index a694003..85ba198 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -99,6 +99,55 @@ describe('execute', () => { expect(got2).toEqual(want) }) + test('it properly returns an error', async () => { + const mockError = { + message: + 'target: test.0.primary: vttablet: rpc error: code = NotFound desc = Table \'vt_test_0.foo\' doesn\'t exist (errno 1146) (sqlstate 42S02) (CallerID: unsecure_grpc_client): Sql: "select * from foo", BindVars: {#maxLimit: "type:INT64 value:\\"10001\\""}', + code: 'NOT_FOUND' + } + + const mockResponse = { + session: { + signature: '5HEp/jX+n/wwWrpHawlSHuGIXYKTZLPCYh+95XVdYsk=', + vitessSession: { + autocommit: true, + options: { + includedFields: 'ALL', + clientFoundRows: true + }, + foundRows: '3', + rowCount: '-1', + DDLStrategy: 'direct', + SessionUUID: '6XKXT5XYfiawc1Iq2n2BHg', + enableSystemSettings: true + } + }, + error: mockError + } + + mockPool + .intercept({ + path: EXECUTE_PATH, + method: 'POST' + }) + .reply(200, mockResponse) + + const want: ExecutedQuery = { + headers: [], + rows: [], + size: 0, + error: mockError, + statement: 'SELECT * from foo;', + time: 1 + } + + const connection = connect(config) + const got = await connection.execute('SELECT * from foo;') + got.time = 1 + + expect(got).toEqual(want) + }) + test('it properly escapes query parameters', async () => { const mockResponse = { session: null, From ff4615b40e199bf78036f0b7529e50c0c3de024d Mon Sep 17 00:00:00 2001 From: Iheanyi Ekechukwu Date: Wed, 3 Aug 2022 12:23:29 -0500 Subject: [PATCH 09/12] Coalesce error as null if it is undefined. --- __tests__/index.test.ts | 2 ++ src/index.ts | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index 85ba198..4ffbe27 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -63,6 +63,7 @@ describe('execute', () => { ':vtg1': 1 } ], + error: null, size: 1, statement: 'SELECT 1 from dual;', time: 1 @@ -175,6 +176,7 @@ describe('execute', () => { } ], size: 1, + error: null, statement: "SELECT 1 from dual where foo = 'bar';", time: 1 } diff --git a/src/index.ts b/src/index.ts index e051c4d..70ee307 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,7 +18,7 @@ export interface ExecutedQuery { rows: Row[] size: number statement: string - error?: VitessError | null + error: VitessError | null time: number } @@ -150,7 +150,7 @@ export class Connection { return { headers, rows, - error, + error: error ?? null, size: rows.length, statement: query, time From 98a52715779ad05f8cfa3b0244818643cbbafd88 Mon Sep 17 00:00:00 2001 From: Iheanyi Ekechukwu Date: Wed, 3 Aug 2022 12:43:53 -0500 Subject: [PATCH 10/12] Return a simple number for session, it's opaque to us. --- __tests__/index.test.ts | 59 ++++++++--------------------------------- 1 file changed, 11 insertions(+), 48 deletions(-) diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index 4ffbe27..9a40bbd 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -19,25 +19,10 @@ setGlobalDispatcher(mockAgent) // Provide the base url to the request const mockPool = mockAgent.get(mockHost) +const mockSession = 42 describe('execute', () => { - test('it properly returns and decodes a response', async () => { - const mockSession = { - signature: 'V6cmWP8EOlhUQFB1Ca/IsRQoKGDpHmuNhAdn1ObLrCE=', - vitessSession: { - autocommit: true, - options: { - includedFields: 'ALL', - clientFoundRows: true - }, - foundRows: '1', - rowCount: '-1', - DDLStrategy: 'direct', - SessionUUID: 'dbtDuhIRDpZPzDUkgXIuzg', - enableSystemSettings: true - } - } - + test('it properly returns and decodes a select query', async () => { const mockResponse = { session: mockSession, result: { @@ -74,7 +59,12 @@ describe('execute', () => { path: EXECUTE_PATH, method: 'POST' }) - .reply(200, mockResponse) + .reply(200, (opts) => { + expect(opts.headers).toContain('authorization') + 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;') @@ -100,7 +90,7 @@ describe('execute', () => { expect(got2).toEqual(want) }) - test('it properly returns an error', async () => { + test('it properly returns an error from the API', async () => { const mockError = { message: 'target: test.0.primary: vttablet: rpc error: code = NotFound desc = Table \'vt_test_0.foo\' doesn\'t exist (errno 1146) (sqlstate 42S02) (CallerID: unsecure_grpc_client): Sql: "select * from foo", BindVars: {#maxLimit: "type:INT64 value:\\"10001\\""}', @@ -108,21 +98,7 @@ describe('execute', () => { } const mockResponse = { - session: { - signature: '5HEp/jX+n/wwWrpHawlSHuGIXYKTZLPCYh+95XVdYsk=', - vitessSession: { - autocommit: true, - options: { - includedFields: 'ALL', - clientFoundRows: true - }, - foundRows: '3', - rowCount: '-1', - DDLStrategy: 'direct', - SessionUUID: '6XKXT5XYfiawc1Iq2n2BHg', - enableSystemSettings: true - } - }, + session: mockSession, error: mockError } @@ -203,26 +179,13 @@ describe('execute', () => { describe('refresh', () => { test('it sets the session variable when true', async () => { const connection = connect(config) - const mockSession = { - signature: 'testvitesssession', - vitessSession: { - autocommit: true, - options: { - includedFields: 'ALL', - clientFoundRows: true - }, - DDLStrategy: 'direct', - SessionUUID: 'Z2zXmUvMs64GwM9pcaUMhQ', - enableSystemSettings: true - } - } mockPool .intercept({ path: CREATE_SESSION_PATH, method: 'POST' }) - .reply(200, mockSession) + .reply(200, JSON.stringify(mockSession)) const got = await connection.refresh() From c1310426810c9dae1c140f8b7e0111e67b9b66b9 Mon Sep 17 00:00:00 2001 From: Iheanyi Ekechukwu Date: Wed, 3 Aug 2022 13:02:37 -0500 Subject: [PATCH 11/12] Always return the rowsAffected and insertId when possible. --- __tests__/index.test.ts | 97 ++++++++++++++++++++++++++++++++++++++++- src/index.ts | 10 ++++- 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index 9a40bbd..c1bf7ba 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -51,7 +51,9 @@ describe('execute', () => { error: null, size: 1, statement: 'SELECT 1 from dual;', - time: 1 + time: 1, + rowsAffected: null, + insertId: null } mockPool @@ -90,6 +92,95 @@ describe('execute', () => { expect(got2).toEqual(want) }) + test('it properly returns an executed query for a DDL statement', async () => { + const mockResponse = { + session: mockSession, + result: {} + } + + mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, mockResponse) + + const query = 'CREATE TABLE `foo` (bar json);' + const want: ExecutedQuery = { + headers: [], + rows: [], + rowsAffected: null, + insertId: null, + error: null, + size: 0, + statement: query, + time: 1 + } + + const connection = connect(config) + + const got = await connection.execute(query) + got.time = 1 + + expect(got).toEqual(want) + }) + + test('it properly returns an executed query for an UPDATE statement', async () => { + const mockResponse = { + session: mockSession, + result: { + rowsAffected: '1' + } + } + + mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, mockResponse) + + const query = "UPDATE `foo` SET bar='planetscale'" + const want: ExecutedQuery = { + headers: [], + rows: [], + rowsAffected: 1, + insertId: null, + error: null, + size: 0, + statement: query, + time: 1 + } + + const connection = connect(config) + + const got = await connection.execute(query) + got.time = 1 + + expect(got).toEqual(want) + }) + + test('it properly returns an executed query for an INSERT statement', async () => { + const mockResponse = { + session: mockSession, + result: { + rowsAffected: '1', + insertId: '2' + } + } + + mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, mockResponse) + + const query = "INSERT INTO `foo` (bar) VALUES ('planetscale');" + const want: ExecutedQuery = { + headers: [], + rows: [], + rowsAffected: 1, + insertId: '2', + error: null, + size: 0, + statement: query, + time: 1 + } + + const connection = connect(config) + + const got = await connection.execute(query) + got.time = 1 + + expect(got).toEqual(want) + }) + test('it properly returns an error from the API', async () => { const mockError = { message: @@ -113,6 +204,8 @@ describe('execute', () => { headers: [], rows: [], size: 0, + insertId: null, + rowsAffected: null, error: mockError, statement: 'SELECT * from foo;', time: 1 @@ -153,6 +246,8 @@ describe('execute', () => { ], size: 1, error: null, + insertId: null, + rowsAffected: null, statement: "SELECT 1 from dual where foo = 'bar';", time: 1 } diff --git a/src/index.ts b/src/index.ts index 70ee307..98a3c8d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,8 @@ export interface ExecutedQuery { rows: Row[] size: number statement: string + insertId: string + rowsAffected: number error: VitessError | null time: number } @@ -62,8 +64,8 @@ interface QueryExecuteResponse { } interface QueryResult { - rowsAffected?: number | null - insertId?: number | null + rowsAffected?: string | null + insertId?: string | null fields?: QueryResultField[] | null rows?: QueryResultRow[] } @@ -141,6 +143,8 @@ export class Connection { const time = Date.now() - startTime const { result, session, error } = saved + const rowsAffected = result?.rowsAffected ? parseInt(result.rowsAffected, 10) : null + const insertId = result?.insertId ?? null this.session = session @@ -150,6 +154,8 @@ export class Connection { return { headers, rows, + rowsAffected, + insertId, error: error ?? null, size: rows.length, statement: query, From d8d1f6c258b9fb538dc2e599c6db062bc4849834 Mon Sep 17 00:00:00 2001 From: Iheanyi Ekechukwu Date: Wed, 3 Aug 2022 13:02:47 -0500 Subject: [PATCH 12/12] insertId and rowsAffected could be null. --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 98a3c8d..a8c28f1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,8 +18,8 @@ export interface ExecutedQuery { rows: Row[] size: number statement: string - insertId: string - rowsAffected: number + insertId: string | null + rowsAffected: number | null error: VitessError | null time: number }