Skip to content
Merged
175 changes: 142 additions & 33 deletions __tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -63,17 +48,25 @@ describe('execute', () => {
':vtg1': 1
}
],
error: null,
size: 1,
statement: 'SELECT 1 from dual;',
time: 1
time: 1,
rowsAffected: null,
insertId: null
}

mockPool
.intercept({
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;')
Expand All @@ -99,6 +92,132 @@ 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:
'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: mockSession,
error: mockError
}

mockPool
.intercept({
path: EXECUTE_PATH,
method: 'POST'
})
.reply(200, mockResponse)

const want: ExecutedQuery = {
headers: [],
rows: [],
size: 0,
insertId: null,
rowsAffected: null,
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,
Expand Down Expand Up @@ -126,6 +245,9 @@ describe('execute', () => {
}
],
size: 1,
error: null,
insertId: null,
rowsAffected: null,
statement: "SELECT 1 from dual where foo = 'bar';",
time: 1
}
Expand All @@ -152,26 +274,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()

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
73 changes: 40 additions & 33 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,37 @@ type ReqInit = Pick<RequestInit, 'method' | 'headers'> & {
body: string
}

type Row = Record<string, unknown>

interface VitessError {
message: string
code: string
}

export interface ExecutedQuery {
headers: string[]
rows: Row[]
size: number
statement: string
insertId: string | null
rowsAffected: number | null
error: VitessError | null
time: number
}

export interface Config {
username: string
password: string
host: string
fetch?: (input: string, init?: ReqInit) => Promise<Pick<Response, 'ok' | 'json' | 'status' | 'statusText' | 'text'>>
}

export interface QueryResultRow {
interface QueryResultRow {
lengths: string[]
values: string
}

export interface QueryResultField {
interface QueryResultField {
name?: string
type?: string
table?: string
Expand All @@ -37,28 +55,23 @@ export interface QueryResultField {
columnType?: string | null
}

export type QuerySession = unknown

interface VitessError {
message: string
code: string
}
type QuerySession = unknown

export interface QueryExecuteResponse {
interface QueryExecuteResponse {
session: QuerySession
result: QueryResult | null
error?: VitessError
}

export interface QueryResult {
rowsAffected?: number | null
insertId?: number | null
interface QueryResult {
rowsAffected?: string | null
insertId?: string | null
fields?: QueryResultField[] | null
rows?: QueryResultRow[]
}

export class Client {
config: Config
private config: Config

constructor(config: Config) {
this.config = config
Expand All @@ -73,10 +86,6 @@ export class Client {
}
}

export function connect(config: Config): Connection {
return new Connection(config)
}

export class Connection {
private config: Config
private session: QuerySession | null
Expand Down Expand Up @@ -117,8 +126,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}`)
}
Expand All @@ -135,7 +143,8 @@ export class Connection {
const time = Date.now() - startTime

const { result, session, error } = saved
if (error) throw new Error(error.message)
const rowsAffected = result?.rowsAffected ? parseInt(result.rowsAffected, 10) : null
const insertId = result?.insertId ?? null

this.session = session

Expand All @@ -145,13 +154,20 @@ export class Connection {
return {
headers,
rows,
rowsAffected,
insertId,
error: error ?? null,
size: rows.length,
statement: query,
time
}
}
}

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) => {
Expand Down Expand Up @@ -196,20 +212,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<string, unknown>

export interface ExecutedQuery {
headers?: string[]
rows?: Row[]
size?: number
statement?: string
rawError?: Error
errorCode?: string
errorMessage?: string
time?: number
}