Skip to content

Commit 529da93

Browse files
authored
improve type inference of execute result (#72)
1 parent 590a9a2 commit 529da93

5 files changed

Lines changed: 106 additions & 58 deletions

File tree

integration-test/basic.test.ts

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const table = 'employee'
88

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

11+
function assertType<T>(_value: T): void {}
12+
1113
beforeAll(async () => {
1214
dotenv.config()
1315
databaseURL = process.env.DATABASE_URL
@@ -22,6 +24,9 @@ describe('basic', () => {
2224
test('ddl', async () => {
2325
const con = connect({ url: databaseURL, database: database, fetch, debug: true })
2426
const results = await con.execute(`SHOW TABLES`)
27+
28+
assertType<Row[]>(results)
29+
2530
expect(JSON.stringify(results)).toContain(`${table}`)
2631
})
2732

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

3237
await con.execute(`insert into ${table} values (1, 'John', 'Doe')`)
3338
const result1 = await con.execute(`select * from ${table} where emp_no = 1`)
39+
40+
assertType<Row[]>(result1)
3441
expect(JSON.stringify(result1)).toContain('John')
42+
3543
await con.execute(`update ${table} set first_name = 'Jane' where emp_no = 1`)
3644
const result2 = await con.execute(`select * from ${table} where emp_no = 1`)
45+
46+
assertType<Row[]>(result2)
3747
expect(JSON.stringify(result2)).toContain('Jane')
48+
3849
await con.execute(`delete from ${table} where emp_no = 1`)
39-
const result3 = (await con.execute(`select * from ${table} where emp_no = 1`)) as Row[]
50+
const result3 = await con.execute(`select * from ${table} where emp_no = 1`)
51+
52+
assertType<Row[]>(result3)
4053
expect(result3.length).toEqual(0)
4154
})
4255

4356
test('option', async () => {
4457
const con = connect({ url: databaseURL, database: database, fetch, debug: true })
4558
const result1 = await con.execute(`select * from ${table} where emp_no=0`, null, { arrayMode: true })
4659
const result2 = await con.execute(`select * from ${table} where emp_no=0`, null, { fullResult: true })
47-
const except1: Row[] = [[0, 'base', 'base']]
48-
const except2: FullResult = {
60+
61+
assertType<Row[]>(result1)
62+
assertType<FullResult>(result2)
63+
64+
const expect1: Row[] = [[0, 'base', 'base']]
65+
const expect2: FullResult = {
4966
statement: `select * from ${table} where emp_no=0`,
5067
types: {
5168
emp_no: 'INT',
@@ -63,26 +80,35 @@ describe('basic', () => {
6380
lastInsertId: null,
6481
rowCount: 1
6582
}
66-
expect(JSON.stringify(result1)).toEqual(JSON.stringify(except1))
67-
expect(JSON.stringify(result2)).toEqual(JSON.stringify(except2))
83+
84+
expect(JSON.stringify(result1)).toEqual(JSON.stringify(expect1))
85+
expect(JSON.stringify(result2)).toEqual(JSON.stringify(expect2))
6886
})
6987

7088
test('arrayMode with config and option', async () => {
7189
const con = connect({ url: databaseURL, database: database, fetch, arrayMode: true, debug: true })
7290
const result1 = await con.execute(`select * from ${table} where emp_no=0`, null, { arrayMode: false })
7391
const result2 = await con.execute(`select * from ${table} where emp_no=0`)
74-
const except1: Row[] = [{ emp_no: 0, first_name: 'base', last_name: 'base' }]
75-
const except2: Row[] = [[0, 'base', 'base']]
76-
expect(JSON.stringify(result1)).toEqual(JSON.stringify(except1))
77-
expect(JSON.stringify(result2)).toEqual(JSON.stringify(except2))
92+
93+
assertType<Row[]>(result1)
94+
assertType<Row[]>(result2)
95+
96+
const expect1: Row[] = [{ emp_no: 0, first_name: 'base', last_name: 'base' }]
97+
const expect2: Row[] = [[0, 'base', 'base']]
98+
expect(JSON.stringify(result1)).toEqual(JSON.stringify(expect1))
99+
expect(JSON.stringify(result2)).toEqual(JSON.stringify(expect2))
78100
})
79101

80102
test('fullResult with config and option', async () => {
81103
const con = connect({ url: databaseURL, database: database, fetch, fullResult: true, debug: true })
82104
const result1 = await con.execute(`select * from ${table} where emp_no=0`, null, { fullResult: false })
83105
const result2 = await con.execute(`select * from ${table} where emp_no=0`)
84-
const except1: Row[] = [{ emp_no: 0, first_name: 'base', last_name: 'base' }]
85-
const except2: FullResult = {
106+
107+
assertType<Row[]>(result1)
108+
assertType<FullResult>(result2)
109+
110+
const expect1: Row[] = [{ emp_no: 0, first_name: 'base', last_name: 'base' }]
111+
const expect2: FullResult = {
86112
statement: `select * from ${table} where emp_no=0`,
87113
types: {
88114
emp_no: 'INT',
@@ -100,8 +126,8 @@ describe('basic', () => {
100126
lastInsertId: null,
101127
rowCount: 1
102128
}
103-
expect(JSON.stringify(result1)).toEqual(JSON.stringify(except1))
104-
expect(JSON.stringify(result2)).toEqual(JSON.stringify(except2))
129+
expect(JSON.stringify(result1)).toEqual(JSON.stringify(expect1))
130+
expect(JSON.stringify(result2)).toEqual(JSON.stringify(expect2))
105131
})
106132

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

113139
// "select * from employee where first_name = '\\'John\\''"
114-
const r1 = (await con.execute('select * from employee where first_name = ?', ["'John'"])) as Row[]
140+
const r1 = await con.execute('select * from employee where first_name = ?', ["'John'"])
115141
// 'select * from employee where first_name = \'\\"John\\"\''
116-
const r2 = (await con.execute('select * from employee where first_name =:name', { name: '"John"' })) as Row[]
142+
const r2 = await con.execute('select * from employee where first_name =:name', { name: '"John"' })
143+
144+
assertType<Row[]>(r1)
145+
assertType<Row[]>(r2)
146+
117147
expect(r1.length).toEqual(1)
118148
expect(r2.length).toEqual(1)
119149
const row1 = r1[0] as Record<string, any>
@@ -129,8 +159,12 @@ describe('basic', () => {
129159
try {
130160
tx = await con.begin()
131161
await tx.execute(`insert into ${table} values (1, 'John', 'Doe')`)
132-
const r1 = (await tx.execute(`select * from ${table} where emp_no = 1`)) as Row[]
133-
const r2 = (await con.execute(`select * from ${table} where emp_no = 1`)) as Row[]
162+
const r1 = await tx.execute(`select * from ${table} where emp_no = 1`)
163+
const r2 = await con.execute(`select * from ${table} where emp_no = 1`)
164+
165+
assertType<Row[]>(r1)
166+
assertType<Row[]>(r2)
167+
134168
expect(r1.length).toEqual(1)
135169
expect(r2.length).toEqual(0)
136170
await tx.commit()
@@ -170,10 +204,12 @@ describe('basic', () => {
170204
await con.execute(`delete from ${table} where emp_no = 1`)
171205

172206
const tx = await con.begin({ isolation: 'READ COMMITTED' })
173-
const result1 = (await tx.execute(`select * from ${table}`)) as Row[]
207+
const result1 = await tx.execute(`select * from ${table}`, null)
208+
assertType<Row[]>(result1)
174209
await con.execute(`insert into ${table} values (1, '\\"John\\"', 'Doe')`)
175-
const result2 = (await tx.execute(`select * from ${table}`)) as Row[]
210+
const result2 = await tx.execute(`select * from ${table}`, null, { fullResult: true })
211+
assertType<FullResult>(result2)
176212
await tx.commit()
177-
expect(result1.length + 1).toEqual(result2.length)
213+
expect(result1.length + 1).toEqual(result2.rows?.length ?? result2.rowCount)
178214
})
179215
})

integration-test/type.test.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { connect, Row, FullResult } from '../dist/index'
1+
import { connect } from '../dist/index'
22
import { fetch } from 'undici'
33
import * as dotenv from 'dotenv'
44
import { uint8ArrayToHex } from '../src/format'
@@ -154,16 +154,17 @@ describe('types', () => {
154154
const con = connect({ url: databaseURL, database: database, fetch, debug: true })
155155
await con.execute(`delete from ${table}`)
156156
await con.execute('insert into multi_data_type values ()')
157-
const r = (await con.execute('select * from multi_data_type', null, { fullResult: true })) as FullResult
158-
expect(r.rows.length).toEqual(1)
159-
expect(JSON.stringify(r.rows[0])).toEqual(JSON.stringify(nullResult))
157+
const r = await con.execute('select * from multi_data_type', null, { fullResult: true })
158+
159+
expect(r.rows?.length).toEqual(1)
160+
expect(JSON.stringify(r.rows?.[0])).toEqual(JSON.stringify(nullResult))
160161
})
161162

162163
test('test all types', async () => {
163164
const con = connect({ url: databaseURL, database: database, fetch })
164165
await con.execute(`delete from ${table}`)
165166
await con.execute(insertSQL)
166-
const rows = (await con.execute('select * from multi_data_type')) as Row[]
167+
const rows = await con.execute('select * from multi_data_type')
167168
expect(rows.length).toEqual(1)
168169
// binary type returns Uint8Array, encode with base64
169170
rows[0]['c_binary'] = Buffer.from(rows[0]['c_binary']).toString('base64')
@@ -192,7 +193,7 @@ describe('types', () => {
192193
const input = 'FSDF'
193194
const inputAsBuffer = Buffer.from(input, 'base64')
194195
await con.execute(`insert into ${tableName} values (?)`, [inputAsBuffer])
195-
const rows = (await con.execute(`select * from ${tableName}`)) as Row[]
196+
const rows = await con.execute(`select * from ${tableName}`)
196197

197198
console.log(rows)
198199
expect(rows.length).toEqual(1)

package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
"jest": "^29.6.2",
5858
"ts-jest": "^29.1.1",
5959
"ts-node": "^10.9.1",
60-
"typescript": "^4.9.5",
60+
"typescript": "^5.6.3",
6161
"undici": "^5.26.2",
6262
"tsup": "^7.1.0"
6363
},
@@ -82,7 +82,8 @@
8282
],
8383
"@typescript-eslint/no-explicit-any": "off",
8484
"@typescript-eslint/no-empty-function": "off",
85-
"@typescript-eslint/ban-ts-comment": "off"
85+
"@typescript-eslint/ban-ts-comment": "off",
86+
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
8687
},
8788
"root": true,
8889
"env": {

src/index.ts

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -35,36 +35,44 @@ interface QueryExecuteResponse {
3535

3636
const defaultExecuteOptions: ExecuteOptions = {}
3737

38-
export class Tx {
39-
private conn: Connection
38+
export class Tx<T extends Config> {
39+
private conn: Connection<T>
4040

41-
constructor(conn: Connection) {
41+
constructor(conn: Connection<T>) {
4242
this.conn = conn
4343
}
4444

45-
async execute(
45+
async execute<E extends ExecuteOptions>(
4646
query: string,
4747
args: ExecuteArgs = null,
48-
options: ExecuteOptions = defaultExecuteOptions,
48+
options: E = defaultExecuteOptions as E,
4949
txOptions: TxOptions = {}
50-
): Promise<FullResult | Row[]> {
50+
): Promise<ExecuteResult<E, T>> {
5151
return this.conn.execute(query, args, options, txOptions)
5252
}
5353

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

58-
async rollback(): Promise<FullResult | Row[]> {
58+
async rollback(): Promise<T['fullResult'] extends true ? FullResult : Row[]> {
5959
return this.conn.execute('ROLLBACK')
6060
}
6161
}
6262

63-
export class Connection {
64-
private config: Config
63+
export type ExecuteResult<E extends ExecuteOptions, T extends Config> = E extends { fullResult: boolean }
64+
? E['fullResult'] extends true
65+
? FullResult
66+
: Row[]
67+
: T['fullResult'] extends true
68+
? FullResult
69+
: Row[]
70+
71+
export class Connection<T extends Config> {
72+
private config: T
6573
private session: Session
6674

67-
constructor(config: Config) {
75+
constructor(config: T) {
6876
this.session = null
6977
this.config = { ...config }
7078

@@ -93,19 +101,19 @@ export class Connection {
93101
return this.config
94102
}
95103

96-
async begin(txOptions: TxOptions = {}): Promise<Tx> {
97-
const conn = new Connection(this.config)
98-
const tx = new Tx(conn)
99-
await tx.execute('BEGIN', undefined, undefined, txOptions)
104+
async begin(txOptions: TxOptions = {}) {
105+
const conn = new Connection<T>(this.config)
106+
const tx = new Tx<T>(conn)
107+
await tx.execute<T>('BEGIN', undefined, undefined, txOptions)
100108
return tx
101109
}
102110

103-
async execute(
111+
async execute<E extends ExecuteOptions>(
104112
query: string,
105113
args: ExecuteArgs = null,
106-
options: ExecuteOptions = defaultExecuteOptions,
114+
options: E = defaultExecuteOptions as E,
107115
txOptions: TxOptions = {}
108-
): Promise<FullResult | Row[]> {
116+
): Promise<ExecuteResult<E, T>> {
109117
const sql = args ? format(query, args) : query
110118
const body = JSON.stringify({ query: sql })
111119
const debug = options.debug ?? this.config.debug ?? false
@@ -144,17 +152,19 @@ export class Connection {
144152
rowsAffected,
145153
lastInsertId,
146154
rowCount: rows.length
147-
}
155+
} as ExecuteResult<E, T>
148156
}
149-
return rows
157+
158+
return rows as ExecuteResult<E, T>
150159
}
151160
}
152161

153-
export function connect(config: Config): Connection {
154-
return new Connection(config)
162+
export function connect<T extends Config>(config: T): Connection<T> {
163+
return new Connection<T>(config)
155164
}
156165

157166
type Cast = typeof cast
167+
158168
function parseArrayRow(fields: Field[], rawRow: string[], cast: Cast, decoders: Decoders): Row {
159169
return fields.map((field, ix) => {
160170
return cast(field, rawRow[ix], decoders)

0 commit comments

Comments
 (0)