Skip to content

Commit 75661fa

Browse files
authored
implement stateful connection (#81)
1 parent a363cce commit 75661fa

6 files changed

Lines changed: 107 additions & 16 deletions

File tree

README.md

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
This driver is for serverless and edge compute platforms that require HTTP external connections, such as Vercel Edge Functions or Cloudflare Workers.
44

5+
There are three ways to use the driver:
6+
7+
1. Stateless connection (default): each query is independent, ideal for edge environments with short-lived, frequently created connections.
8+
2. Stateful connection (experimental): use it when you require session.
9+
3. Transaction (experimental): use it when you require interactive transaction.
10+
511
## Usage
612

713
**Install**
@@ -12,9 +18,9 @@ You can install the driver with npm:
1218
npm install @tidbcloud/serverless
1319
```
1420

15-
**Query**
21+
**Stateless Connection**
1622

17-
To query from TiDB Serverless, you need to create a connection first. Then you can use the connection to execute raw SQL queries. For example:
23+
To query from TiDB Serverless, you need to create a connection first. Then you can use the connection to execute raw SQL queries.
1824

1925
```ts
2026
import { connect } from '@tidbcloud/serverless'
@@ -23,10 +29,38 @@ const conn = connect({url: 'mysql://username:password@host/database'})
2329
const results = await conn.execute('select * from test where id = ?',[1])
2430
```
2531

26-
**Transaction (Experimental)**
32+
**Stateful Connection (experimental)**
33+
34+
If you want to keep session state across multiple queries, create a stateful connection. Remember to call `close()` to release the connection, or you may reach the connection limits.
35+
36+
> **Note:**
37+
>
38+
> Connections idle for 10 minutes will be closed automatically.
39+
> The Stateful connection is not concurrent-safe. You are not allowed to run SQLs parallel in the same stateful connection.
40+
41+
```ts
42+
import { connect } from '@tidbcloud/serverless'
43+
44+
const conn = connect({url: 'mysql://username:password@host/database'})
45+
const stateful = await conn.persist()
46+
47+
try {
48+
const r1 = await stateful.execute('use db2')
49+
const r2 = await stateful.execute('select * from test where id = ?', [2])
50+
} finally {
51+
await stateful.close()
52+
}
53+
```
54+
55+
**Transaction (experimental)**
2756

2857
You can also perform interactive transactions with the serverless driver. For example:
2958

59+
> **Note:**
60+
>
61+
> Transactions idle for 10 minutes will be rolled back automatically if it has not been committed or rolled back.
62+
> The transaction is not concurrent-safe. You are not allowed to run SQLs parallel in the same transaction.
63+
3064
```ts
3165
import { connect } from '@tidbcloud/serverless'
3266

@@ -43,10 +77,6 @@ try {
4377
}
4478
```
4579

46-
> **Note:**
47-
>
48-
> The transaction is not concurrent-safe. You are not allowed to run SQLs parallel in the same transaction.
49-
5080
**Edge example**
5181

5282
The serverless driver is suitable for the edge environments. See how to use it with Vercel Edge Functions:

integration-test/basic.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,4 +212,34 @@ describe('basic', () => {
212212
await tx.commit()
213213
expect(result1.length + 1).toEqual(result2.rows?.length ?? result2.rowCount)
214214
})
215+
216+
test('stateful connection normal flow', async () => {
217+
const conn = connect({ url: databaseURL, database: database, fetch, debug: true })
218+
const stateful = await conn.persist()
219+
220+
await stateful.execute(`use mysql`)
221+
await expect(stateful.execute(`select * from ${table} where emp_no = 0`)).rejects.toThrow()
222+
223+
await stateful.execute(`use ${database}`)
224+
const r = (await stateful.execute(`select * from ${table} where emp_no = 0`)) as Row[]
225+
expect(r.length).toEqual(1)
226+
227+
await stateful.close()
228+
})
229+
230+
test('stateful connection use after close', async () => {
231+
const conn = connect({ url: databaseURL, database: database, fetch, debug: true })
232+
const stateful = await conn.persist()
233+
234+
const r1 = (await stateful.execute(`select * from ${table} where emp_no = 0`)) as Row[]
235+
expect(r1.length).toEqual(1)
236+
237+
await stateful.close()
238+
239+
await expect(stateful.execute(`select * from ${table} where emp_no = 0`)).rejects.toThrow()
240+
241+
// original connection should still work
242+
const r2 = (await conn.execute(`select * from ${table} where emp_no = 0`)) as Row[]
243+
expect(r2.length).toEqual(1)
244+
})
215245
})

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tidbcloud/serverless",
3-
"version": "0.2.0",
3+
"version": "0.3.0",
44
"description": "TiDB Cloud Serverless Driver",
55
"main": "./dist/index.cjs",
66
"module": "./dist/index.js",

src/index.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,9 @@ export class Tx<T extends Config> {
4545
async execute<E extends ExecuteOptions>(
4646
query: string,
4747
args: ExecuteArgs = null,
48-
options: E = defaultExecuteOptions as E,
49-
txOptions: TxOptions = {}
48+
options: E = defaultExecuteOptions as E
5049
): Promise<ExecuteResult<E, T>> {
51-
return this.conn.execute(query, args, options, txOptions)
50+
return this.conn.execute(query, args, options)
5251
}
5352

5453
async commit(): Promise<T['fullResult'] extends true ? FullResult : Row[]> {
@@ -104,15 +103,23 @@ export class Connection<T extends Config> {
104103
async begin(txOptions: TxOptions = {}) {
105104
const conn = new Connection<T>(this.config)
106105
const tx = new Tx<T>(conn)
107-
await tx.execute<T>('BEGIN', undefined, undefined, txOptions)
106+
await conn.execute('BEGIN', undefined, undefined, txOptions)
108107
return tx
109108
}
110109

110+
async persist() {
111+
const conn = new Connection<T>(this.config)
112+
await conn.execute('', null, defaultExecuteOptions as ExecuteOptions, {}, 'open')
113+
const stateful = new StatefulConnection<T>(conn)
114+
return stateful
115+
}
116+
111117
async execute<E extends ExecuteOptions>(
112118
query: string,
113119
args: ExecuteArgs = null,
114120
options: E = defaultExecuteOptions as E,
115-
txOptions: TxOptions = {}
121+
txOptions: TxOptions = {},
122+
statefulAction?: 'open' | 'close'
116123
): Promise<ExecuteResult<E, T>> {
117124
const sql = args ? format(query, args) : query
118125
const body = JSON.stringify({ query: sql })
@@ -125,7 +132,8 @@ export class Connection<T extends Config> {
125132
body,
126133
this.session ?? '',
127134
sql == 'BEGIN' ? txOptions.isolation : null,
128-
debug
135+
debug,
136+
statefulAction
129137
)
130138

131139
this.session = resp?.session ?? null
@@ -159,6 +167,26 @@ export class Connection<T extends Config> {
159167
}
160168
}
161169

170+
export class StatefulConnection<T extends Config> {
171+
private conn: Connection<T>
172+
173+
constructor(conn: Connection<T>) {
174+
this.conn = conn
175+
}
176+
177+
async execute<E extends ExecuteOptions>(
178+
query: string,
179+
args: ExecuteArgs = null,
180+
options: E = defaultExecuteOptions as E
181+
): Promise<ExecuteResult<E, T>> {
182+
return this.conn.execute(query, args, options)
183+
}
184+
185+
async close(): Promise<void> {
186+
await this.conn.execute('', null, defaultExecuteOptions as ExecuteOptions, {}, 'close')
187+
}
188+
}
189+
162190
export function connect<T extends Config>(config: T): Connection<T> {
163191
return new Connection<T>(config)
164192
}

src/serverless.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Config } from './config.js'
22
import { DatabaseError } from './error.js'
33
import { Version } from './version.js'
4-
export async function postQuery<T>(config: Config, body, session = '', isolationLevel = null, debug): Promise<T> {
4+
export async function postQuery<T>(config: Config, body, session = '', isolationLevel = null, debug, statefulAction?: string): Promise<T> {
55
let fetchCacheOption: Record<string, any> = { cache: 'no-store' }
66
// Cloudflare Workers does not support cache now https://github.com/cloudflare/workerd/issues/69
77
try {
@@ -32,6 +32,9 @@ export async function postQuery<T>(config: Config, body, session = '', isolation
3232
if (isolationLevel) {
3333
headers['TiDB-Isolation-Level'] = isolationLevel
3434
}
35+
if (statefulAction) {
36+
headers['TiDB-Stateful-Action'] = statefulAction
37+
}
3538
const response = await fetch(url.toString(), {
3639
method: 'POST',
3740
body: body,

src/version.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const Version = '0.2.0'
1+
export const Version = '0.3.0'

0 commit comments

Comments
 (0)