Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 11 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const config = {
}

const conn = connect(config)
const results = await conn.execute('select 1 from dual where 1=?', [1])
const results = await conn.execute('select 1 from dual')
console.log(results)
```

Expand Down Expand Up @@ -78,8 +78,8 @@ const config = {

const conn = connect(config)
const results = await conn.transaction(async (tx) => {
const whenBranch = await tx.execute('INSERT INTO branches (database_id, name) VALUES (?, ?)', [42, "planetscale"])
const whenCounter = await tx.execute('INSERT INTO slotted_counters(record_type, record_id, slot, count) VALUES (?, ?, RAND() * 100, 1) ON DUPLICATE KEY UPDATE count = count + 1', ['branch_count', 42])
const whenBranch = await tx.execute(`INSERT INTO branches (database_id, name) VALUES (42, 'planetscale')`)
const whenCounter = await tx.execute(`INSERT INTO slotted_counters(record_type, record_id, slot, count) VALUES ('branch_count', 42, RAND() * 100, 1) ON DUPLICATE KEY UPDATE count = count + 1"`
return [whenBranch, whenCounter]
})
console.log(results)
Expand Down Expand Up @@ -129,34 +129,16 @@ console.log(results)
await disconnectAll()
```

### Custom query parameter format function

Query replacement parameters identified with `?` are replaced with escaped values. Named replacement parameters are supported with a colon prefix.
Placeholders inside string literals, quoted identifiers, and non-executable SQL comments are left unchanged. MySQL `/*! ... */` version comments contain executable SQL and are formatted accordingly. A version comment whose tokenization differs between supported Vitess releases throws an error.
Use `AS` before a quoted select-list alias (for example, `? AS 'value'`) so a quoted value cannot concatenate with the alias.
### Query parameters

```ts
const results1 = await conn.execute('select 1 from dual where 1=?', [42])
const results2 = await conn.execute('select 1 from dual where 1=:id', { id: 42 })
```
`execute` does not interpolate query parameters. Pass it a complete SQL string. Calling `execute` with an array or a named-parameter object throws an error.

Providing a custom format function overrides the built-in placeholder parsing and escaping with an external library, like [`sqlstring`](https://github.com/mysqljs/sqlstring).
`sqlstring` replaces `?` characters inside strings and comments as well as value placeholders, so using it bypasses the context-aware behavior above. Only use a custom formatter whose placeholder rules are safe for your query templates.
Any values you include in a query must be escaped first, for example with [`sql-escaper`](https://github.com/mysqljs/sql-escaper):

```ts
import { connect } from '@planetscale/database'
import SqlString from 'sqlstring'
import { format } from 'sql-escaper'

const config = {
format: SqlString.format,
host: '<host>',
username: '<user>',
password: '<password>'
}

const conn = connect(config)
const results = await conn.execute('select 1 from dual where 1=?', [42])
console.log(results)
const results = await conn.execute(format('select 1 from dual where 1=?', [42]))
```

### Custom type casting function
Expand Down Expand Up @@ -188,7 +170,6 @@ You can also pass a custom `cast` function to `execute`. If present, this will o
```ts
const result = await conn.execute(
'SELECT userId, SUM(balance) AS balance FROM UserBalanceItem GROUP BY userId',
{},
{
cast: (field, value) => {
if (field.name === 'balance') {
Expand All @@ -205,11 +186,11 @@ const result = await conn.execute(
Rows can be returned as an object or an array of column values by passing an `as` option to `execute`.

```ts
const query = 'select 1 as one, 2 as two where 1=?'
const objects = conn.execute(query, [1], { as: 'object' })
const query = 'select 1 as one, 2 as two'
const objects = conn.execute(query, { as: 'object' })
// objects.rows => [{one: '1', two: '2'}]

const arrays = conn.execute(query, [1], { as: 'array' })
const arrays = conn.execute(query, { as: 'array' })
// arrays.rows => [['1', '2']]
```

Expand Down
90 changes: 19 additions & 71 deletions __tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import assert from 'node:assert/strict'
import { describe, test } from 'node:test'
import SqlString from 'sqlstring'
import { connect, format, hex, DatabaseError, UnknownError, type Cast } from '../dist/index'
import { Client, connect, hex, DatabaseError, UnknownError, type Cast } from '../dist/index'
import { fetch, MockAgent, setGlobalDispatcher } from 'undici'
import packageJSON from '../package.json'

Expand Down Expand Up @@ -280,7 +279,7 @@ describe('execute', () => {
})

const connection = connect(config)
const got = await connection.execute('SELECT 1 from dual;', null, { as: 'array' })
const got = await connection.execute('SELECT 1 from dual;', { as: 'array' })

assert.deepStrictEqual(got, want)
})
Expand Down Expand Up @@ -452,72 +451,29 @@ describe('execute', () => {
}
})

test('it properly escapes query parameters', async () => {
const mockResponse = {
session: null,
result: {
fields: [{ name: ':vtg1', type: 'INT32' }],
rows: [{ lengths: ['1'], values: 'MQ==' }]
},
timing: 1
}

const want = {
headers: [':vtg1'],
rows: [{ ':vtg1': 1 }],
types: { ':vtg1': 'INT32' },
fields: [{ name: ':vtg1', type: 'INT32' }],
size: 1,
insertId: '0',
rowsAffected: 0,
statement: "SELECT 1 from dual where foo = 'bar';",
time: 1000
}

mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts: any) => {
const bodyObj = JSON.parse(opts.body.toString())
assert.deepStrictEqual(bodyObj.query, want.statement)
return mockResponse
})

test('it rejects query parameters', async () => {
const connection = connect(config)
const got = await connection.execute('SELECT ? from dual where foo = ?;', [1, 'bar'])
const message = /Query parameters are not supported/

assert.deepStrictEqual(got, want)
await assert.rejects(connection.execute('SELECT ? from dual', [1] as any), message)
await assert.rejects(connection.execute('SELECT :id from dual', { id: 1 } as any), message)
await assert.rejects((connection.execute as any)('SELECT 1 from dual', null, { as: 'array' }), message)
})

test('it uses custom format function', async () => {
const mockResponse = {
session: null,
result: {
fields: [{ name: ':vtg1', type: 'INT32' }],
rows: [{ lengths: ['1'], values: 'MQ==' }]
},
timing: 1
}
test('it rejects query parameters in transactions and clients', async () => {
const message = /Query parameters are not supported/

const want = {
headers: [':vtg1'],
types: { ':vtg1': 'INT32' },
fields: [{ name: ':vtg1', type: 'INT32' }],
rows: [{ ':vtg1': 1 }],
size: 1,
insertId: '0',
rowsAffected: 0,
statement: 'select `login`, `email` from `users` where id = 42',
time: 1000
}

mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, (opts: any) => {
const bodyObj = JSON.parse(opts.body.toString())
assert.deepStrictEqual(bodyObj.query, want.statement)
return mockResponse
})
await assert.rejects(new Client(config).execute('SELECT ? from dual', [1] as any), message)

const connection = connect({ ...config, format: SqlString.format })
const got = await connection.execute('select ?? from ?? where id = ?', [['login', 'email'], 'users', 42])
// BEGIN and ROLLBACK
const mockResponse = { session: null, result: null, timing: 1 }
mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, mockResponse)
mockPool.intercept({ path: EXECUTE_PATH, method: 'POST' }).reply(200, mockResponse)

assert.deepStrictEqual(got, want)
await assert.rejects(
connect(config).transaction((tx) => tx.execute('SELECT ? from dual', [1] as any)),
message
)
})

test('uses custom cast function', async () => {
Expand Down Expand Up @@ -585,7 +541,7 @@ describe('execute', () => {
const connInflate: Cast = (field, value) => (field.type === 'INT64' ? 'I am a biggish int' : value)
const inflate: Cast = (field, value) => (field.type === 'INT64' ? BigInt(value as string) : value)
const connection = connect({ ...config, cast: inflate })
const got = await connection.execute('select 1 from dual', {}, { cast: connInflate })
const got = await connection.execute('select 1 from dual', { cast: connInflate })

assert.deepStrictEqual(got, want)
})
Expand Down Expand Up @@ -635,14 +591,6 @@ describe('refresh', () => {
})
})

describe('format', () => {
test('exports format function', () => {
const query = 'select 1 from user where id=?'
const expected = 'select 1 from user where id=42'
assert.deepStrictEqual(format(query, [42]), expected)
})
})

describe('hex', () => {
test('exports hex function', () => {
assert.deepStrictEqual(hex('\0'), '0x00')
Expand Down
Loading
Loading