Skip to content

Commit

Permalink
improve type inference of execute result (#72)
Browse files Browse the repository at this point in the history
  • Loading branch information
zoubingwu authored Oct 31, 2024
1 parent 590a9a2 commit 529da93
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 58 deletions.
76 changes: 56 additions & 20 deletions integration-test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const table = 'employee'

const EmployeeTable = `CREATE TABLE ${database}.${table} (emp_no INT,first_name VARCHAR(255),last_name VARCHAR(255))`

function assertType<T>(_value: T): void {}

beforeAll(async () => {
dotenv.config()
databaseURL = process.env.DATABASE_URL
Expand All @@ -22,6 +24,9 @@ describe('basic', () => {
test('ddl', async () => {
const con = connect({ url: databaseURL, database: database, fetch, debug: true })
const results = await con.execute(`SHOW TABLES`)

assertType<Row[]>(results)

expect(JSON.stringify(results)).toContain(`${table}`)
})

Expand All @@ -31,21 +36,33 @@ describe('basic', () => {

await con.execute(`insert into ${table} values (1, 'John', 'Doe')`)
const result1 = await con.execute(`select * from ${table} where emp_no = 1`)

assertType<Row[]>(result1)
expect(JSON.stringify(result1)).toContain('John')

await con.execute(`update ${table} set first_name = 'Jane' where emp_no = 1`)
const result2 = await con.execute(`select * from ${table} where emp_no = 1`)

assertType<Row[]>(result2)
expect(JSON.stringify(result2)).toContain('Jane')

await con.execute(`delete from ${table} where emp_no = 1`)
const result3 = (await con.execute(`select * from ${table} where emp_no = 1`)) as Row[]
const result3 = await con.execute(`select * from ${table} where emp_no = 1`)

assertType<Row[]>(result3)
expect(result3.length).toEqual(0)
})

test('option', async () => {
const con = connect({ url: databaseURL, database: database, fetch, debug: true })
const result1 = await con.execute(`select * from ${table} where emp_no=0`, null, { arrayMode: true })
const result2 = await con.execute(`select * from ${table} where emp_no=0`, null, { fullResult: true })
const except1: Row[] = [[0, 'base', 'base']]
const except2: FullResult = {

assertType<Row[]>(result1)
assertType<FullResult>(result2)

const expect1: Row[] = [[0, 'base', 'base']]
const expect2: FullResult = {
statement: `select * from ${table} where emp_no=0`,
types: {
emp_no: 'INT',
Expand All @@ -63,26 +80,35 @@ describe('basic', () => {
lastInsertId: null,
rowCount: 1
}
expect(JSON.stringify(result1)).toEqual(JSON.stringify(except1))
expect(JSON.stringify(result2)).toEqual(JSON.stringify(except2))

expect(JSON.stringify(result1)).toEqual(JSON.stringify(expect1))
expect(JSON.stringify(result2)).toEqual(JSON.stringify(expect2))
})

test('arrayMode with config and option', async () => {
const con = connect({ url: databaseURL, database: database, fetch, arrayMode: true, debug: true })
const result1 = await con.execute(`select * from ${table} where emp_no=0`, null, { arrayMode: false })
const result2 = await con.execute(`select * from ${table} where emp_no=0`)
const except1: Row[] = [{ emp_no: 0, first_name: 'base', last_name: 'base' }]
const except2: Row[] = [[0, 'base', 'base']]
expect(JSON.stringify(result1)).toEqual(JSON.stringify(except1))
expect(JSON.stringify(result2)).toEqual(JSON.stringify(except2))

assertType<Row[]>(result1)
assertType<Row[]>(result2)

const expect1: Row[] = [{ emp_no: 0, first_name: 'base', last_name: 'base' }]
const expect2: Row[] = [[0, 'base', 'base']]
expect(JSON.stringify(result1)).toEqual(JSON.stringify(expect1))
expect(JSON.stringify(result2)).toEqual(JSON.stringify(expect2))
})

test('fullResult with config and option', async () => {
const con = connect({ url: databaseURL, database: database, fetch, fullResult: true, debug: true })
const result1 = await con.execute(`select * from ${table} where emp_no=0`, null, { fullResult: false })
const result2 = await con.execute(`select * from ${table} where emp_no=0`)
const except1: Row[] = [{ emp_no: 0, first_name: 'base', last_name: 'base' }]
const except2: FullResult = {

assertType<Row[]>(result1)
assertType<FullResult>(result2)

const expect1: Row[] = [{ emp_no: 0, first_name: 'base', last_name: 'base' }]
const expect2: FullResult = {
statement: `select * from ${table} where emp_no=0`,
types: {
emp_no: 'INT',
Expand All @@ -100,8 +126,8 @@ describe('basic', () => {
lastInsertId: null,
rowCount: 1
}
expect(JSON.stringify(result1)).toEqual(JSON.stringify(except1))
expect(JSON.stringify(result2)).toEqual(JSON.stringify(except2))
expect(JSON.stringify(result1)).toEqual(JSON.stringify(expect1))
expect(JSON.stringify(result2)).toEqual(JSON.stringify(expect2))
})

test('query with escape', async () => {
Expand All @@ -111,9 +137,13 @@ describe('basic', () => {
await con.execute(`insert into ${table} values (2, '\\"John\\"', 'Doe')`)

// "select * from employee where first_name = '\\'John\\''"
const r1 = (await con.execute('select * from employee where first_name = ?', ["'John'"])) as Row[]
const r1 = await con.execute('select * from employee where first_name = ?', ["'John'"])
// 'select * from employee where first_name = \'\\"John\\"\''
const r2 = (await con.execute('select * from employee where first_name =:name', { name: '"John"' })) as Row[]
const r2 = await con.execute('select * from employee where first_name =:name', { name: '"John"' })

assertType<Row[]>(r1)
assertType<Row[]>(r2)

expect(r1.length).toEqual(1)
expect(r2.length).toEqual(1)
const row1 = r1[0] as Record<string, any>
Expand All @@ -129,8 +159,12 @@ describe('basic', () => {
try {
tx = await con.begin()
await tx.execute(`insert into ${table} values (1, 'John', 'Doe')`)
const r1 = (await tx.execute(`select * from ${table} where emp_no = 1`)) as Row[]
const r2 = (await con.execute(`select * from ${table} where emp_no = 1`)) as Row[]
const r1 = await tx.execute(`select * from ${table} where emp_no = 1`)
const r2 = await con.execute(`select * from ${table} where emp_no = 1`)

assertType<Row[]>(r1)
assertType<Row[]>(r2)

expect(r1.length).toEqual(1)
expect(r2.length).toEqual(0)
await tx.commit()
Expand Down Expand Up @@ -170,10 +204,12 @@ describe('basic', () => {
await con.execute(`delete from ${table} where emp_no = 1`)

const tx = await con.begin({ isolation: 'READ COMMITTED' })
const result1 = (await tx.execute(`select * from ${table}`)) as Row[]
const result1 = await tx.execute(`select * from ${table}`, null)
assertType<Row[]>(result1)
await con.execute(`insert into ${table} values (1, '\\"John\\"', 'Doe')`)
const result2 = (await tx.execute(`select * from ${table}`)) as Row[]
const result2 = await tx.execute(`select * from ${table}`, null, { fullResult: true })
assertType<FullResult>(result2)
await tx.commit()
expect(result1.length + 1).toEqual(result2.length)
expect(result1.length + 1).toEqual(result2.rows?.length ?? result2.rowCount)
})
})
13 changes: 7 additions & 6 deletions integration-test/type.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { connect, Row, FullResult } from '../dist/index'
import { connect } from '../dist/index'
import { fetch } from 'undici'
import * as dotenv from 'dotenv'
import { uint8ArrayToHex } from '../src/format'
Expand Down Expand Up @@ -154,16 +154,17 @@ describe('types', () => {
const con = connect({ url: databaseURL, database: database, fetch, debug: true })
await con.execute(`delete from ${table}`)
await con.execute('insert into multi_data_type values ()')
const r = (await con.execute('select * from multi_data_type', null, { fullResult: true })) as FullResult
expect(r.rows.length).toEqual(1)
expect(JSON.stringify(r.rows[0])).toEqual(JSON.stringify(nullResult))
const r = await con.execute('select * from multi_data_type', null, { fullResult: true })

expect(r.rows?.length).toEqual(1)
expect(JSON.stringify(r.rows?.[0])).toEqual(JSON.stringify(nullResult))
})

test('test all types', async () => {
const con = connect({ url: databaseURL, database: database, fetch })
await con.execute(`delete from ${table}`)
await con.execute(insertSQL)
const rows = (await con.execute('select * from multi_data_type')) as Row[]
const rows = await con.execute('select * from multi_data_type')
expect(rows.length).toEqual(1)
// binary type returns Uint8Array, encode with base64
rows[0]['c_binary'] = Buffer.from(rows[0]['c_binary']).toString('base64')
Expand Down Expand Up @@ -192,7 +193,7 @@ describe('types', () => {
const input = 'FSDF'
const inputAsBuffer = Buffer.from(input, 'base64')
await con.execute(`insert into ${tableName} values (?)`, [inputAsBuffer])
const rows = (await con.execute(`select * from ${tableName}`)) as Row[]
const rows = await con.execute(`select * from ${tableName}`)

console.log(rows)
expect(rows.length).toEqual(1)
Expand Down
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"jest": "^29.6.2",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"typescript": "^4.9.5",
"typescript": "^5.6.3",
"undici": "^5.26.2",
"tsup": "^7.1.0"
},
Expand All @@ -82,7 +82,8 @@
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/ban-ts-comment": "off"
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
},
"root": true,
"env": {
Expand Down
54 changes: 32 additions & 22 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,36 +35,44 @@ interface QueryExecuteResponse {

const defaultExecuteOptions: ExecuteOptions = {}

export class Tx {
private conn: Connection
export class Tx<T extends Config> {
private conn: Connection<T>

constructor(conn: Connection) {
constructor(conn: Connection<T>) {
this.conn = conn
}

async execute(
async execute<E extends ExecuteOptions>(
query: string,
args: ExecuteArgs = null,
options: ExecuteOptions = defaultExecuteOptions,
options: E = defaultExecuteOptions as E,
txOptions: TxOptions = {}
): Promise<FullResult | Row[]> {
): Promise<ExecuteResult<E, T>> {
return this.conn.execute(query, args, options, txOptions)
}

async commit(): Promise<FullResult | Row[]> {
async commit(): Promise<T['fullResult'] extends true ? FullResult : Row[]> {
return this.conn.execute('COMMIT')
}

async rollback(): Promise<FullResult | Row[]> {
async rollback(): Promise<T['fullResult'] extends true ? FullResult : Row[]> {
return this.conn.execute('ROLLBACK')
}
}

export class Connection {
private config: Config
export type ExecuteResult<E extends ExecuteOptions, T extends Config> = E extends { fullResult: boolean }
? E['fullResult'] extends true
? FullResult
: Row[]
: T['fullResult'] extends true
? FullResult
: Row[]

export class Connection<T extends Config> {
private config: T
private session: Session

constructor(config: Config) {
constructor(config: T) {
this.session = null
this.config = { ...config }

Expand Down Expand Up @@ -93,19 +101,19 @@ export class Connection {
return this.config
}

async begin(txOptions: TxOptions = {}): Promise<Tx> {
const conn = new Connection(this.config)
const tx = new Tx(conn)
await tx.execute('BEGIN', undefined, undefined, txOptions)
async begin(txOptions: TxOptions = {}) {
const conn = new Connection<T>(this.config)
const tx = new Tx<T>(conn)
await tx.execute<T>('BEGIN', undefined, undefined, txOptions)
return tx
}

async execute(
async execute<E extends ExecuteOptions>(
query: string,
args: ExecuteArgs = null,
options: ExecuteOptions = defaultExecuteOptions,
options: E = defaultExecuteOptions as E,
txOptions: TxOptions = {}
): Promise<FullResult | Row[]> {
): Promise<ExecuteResult<E, T>> {
const sql = args ? format(query, args) : query
const body = JSON.stringify({ query: sql })
const debug = options.debug ?? this.config.debug ?? false
Expand Down Expand Up @@ -144,17 +152,19 @@ export class Connection {
rowsAffected,
lastInsertId,
rowCount: rows.length
}
} as ExecuteResult<E, T>
}
return rows

return rows as ExecuteResult<E, T>
}
}

export function connect(config: Config): Connection {
return new Connection(config)
export function connect<T extends Config>(config: T): Connection<T> {
return new Connection<T>(config)
}

type Cast = typeof cast

function parseArrayRow(fields: Field[], rawRow: string[], cast: Cast, decoders: Decoders): Row {
return fields.map((field, ix) => {
return cast(field, rawRow[ix], decoders)
Expand Down

0 comments on commit 529da93

Please sign in to comment.