fix: sql guard gaps, date tz, timeouts, payload
This commit is contained in:
@@ -126,6 +126,18 @@ describe('createMysqlDriver', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('sets a connect timeout on every pool', async () => {
|
||||
const driver = createMysqlDriver(target())
|
||||
await driver.query({ sql: 'select 1', rowLimit: 10 })
|
||||
expect(mysqlState.state.pools[0].options.connectTimeout).toBe(10_000)
|
||||
})
|
||||
|
||||
it('returns DATE and DATETIME as strings to avoid tz day-shift', async () => {
|
||||
const driver = createMysqlDriver(target())
|
||||
await driver.query({ sql: 'select 1', rowLimit: 10 })
|
||||
expect(mysqlState.state.pools[0].options.dateStrings).toEqual(['DATE', 'DATETIME'])
|
||||
})
|
||||
|
||||
it('reuses pools per database', async () => {
|
||||
const driver = createMysqlDriver(target())
|
||||
await driver.query({ sql: 'select 1', rowLimit: 10 })
|
||||
|
||||
@@ -41,9 +41,14 @@ const pgState = vi.hoisted(() => {
|
||||
return { state, FakePool }
|
||||
})
|
||||
|
||||
vi.mock('pg', () => ({
|
||||
default: { Pool: pgState.FakePool }
|
||||
}))
|
||||
vi.mock('pg', async () => {
|
||||
// Use the real TypeOverrides so the driver's date/timestamp parser overrides
|
||||
// are exercised against pg's genuine default parsers (e.g. timestamptz).
|
||||
const actual = (await vi.importActual('pg')) as { default: { TypeOverrides: unknown } }
|
||||
return {
|
||||
default: { Pool: pgState.FakePool, TypeOverrides: actual.default.TypeOverrides }
|
||||
}
|
||||
})
|
||||
|
||||
import type { ConnectionConfig } from '../../../src/config/types.js'
|
||||
import { createPostgresDriver } from '../../../src/db/postgres.js'
|
||||
@@ -87,6 +92,27 @@ describe('createPostgresDriver', () => {
|
||||
expect(pgState.state.pools[1].options).toMatchObject({ database: 'other' })
|
||||
})
|
||||
|
||||
it('sets a connection timeout on every pool', async () => {
|
||||
const driver = createPostgresDriver(target())
|
||||
await driver.query({ sql: 'select 1', rowLimit: 10 })
|
||||
expect(pgState.state.pools[0].options.connectionTimeoutMillis).toBe(10_000)
|
||||
})
|
||||
|
||||
it('returns wall-clock date/timestamp types as raw strings, keeps tz absolute', async () => {
|
||||
const driver = createPostgresDriver(target())
|
||||
await driver.query({ sql: 'select 1', rowLimit: 10 })
|
||||
const types = pgState.state.pools[0].options.types as {
|
||||
getTypeParser: (oid: number, format?: string) => (value: string) => unknown
|
||||
}
|
||||
// DATE (1082) and TIMESTAMP without tz (1114) stay raw strings.
|
||||
expect(types.getTypeParser(1082)('2026-06-11')).toBe('2026-06-11')
|
||||
expect(types.getTypeParser(1114)('2026-06-11 12:34:56')).toBe('2026-06-11 12:34:56')
|
||||
// TIMESTAMPTZ (1184) keeps the default parser, which is not identity.
|
||||
expect(types.getTypeParser(1184)('2026-06-11 12:34:56+00')).not.toBe(
|
||||
'2026-06-11 12:34:56+00'
|
||||
)
|
||||
})
|
||||
|
||||
it('passes readonly as a startup option', async () => {
|
||||
const driver = createPostgresDriver(target({ readonly: true }))
|
||||
await driver.query({ sql: 'select 1', rowLimit: 10 })
|
||||
|
||||
@@ -66,6 +66,24 @@ describe('assertSafeStatement', () => {
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('does not treat a digit-leading dollar tag as a dollar quote', () => {
|
||||
// $1$ is a positional param $1 followed by a bare $, NOT a dollar
|
||||
// quote. The ; after it is top-level, so this is two statements.
|
||||
expect(() => assertSafeStatement('SELECT $1$ ; DROP TABLE x', 'postgres')).toThrow(
|
||||
/one SQL statement/
|
||||
)
|
||||
})
|
||||
|
||||
it('allows digits later inside a dollar tag', () => {
|
||||
expect(() =>
|
||||
assertSafeStatement('select $tag1$ a ; b $tag1$', 'postgres')
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('keeps plain positional params with values unaffected', () => {
|
||||
expect(() => assertSafeStatement('SELECT $1', 'postgres')).not.toThrow()
|
||||
})
|
||||
|
||||
it('ignores semicolons inside line comments', () => {
|
||||
expect(() => assertSafeStatement('select 1 -- ; select 2', 'postgres')).not.toThrow()
|
||||
expect(() => assertSafeStatement('select 1 # ; select 2', 'mysql')).not.toThrow()
|
||||
@@ -94,6 +112,25 @@ describe('assertSafeStatement', () => {
|
||||
expect(() => assertSafeStatement("select 'a\\'; select 2'", 'mysql')).not.toThrow()
|
||||
})
|
||||
|
||||
it('handles mysql backslash escape inside double-quoted string', () => {
|
||||
// mysql treats "..." as a string; \" does not close it, so the ;
|
||||
// stays inside the literal and this is a single statement.
|
||||
expect(() => assertSafeStatement('SELECT "x\\";y" AS c', 'mysql')).not.toThrow()
|
||||
})
|
||||
|
||||
it('handles double-quote doubling in both engines', () => {
|
||||
expect(() => assertSafeStatement('select "a""b;c"', 'mysql')).not.toThrow()
|
||||
expect(() => assertSafeStatement('select "a""b;c"', 'postgres')).not.toThrow()
|
||||
})
|
||||
|
||||
it('does not treat backslash as escape in postgres double-quoted identifiers', () => {
|
||||
// For postgres "..." is an identifier with no backslash escapes, so
|
||||
// "a\" closes at the second quote and '; SELECT 1' is a second statement.
|
||||
expect(() => assertSafeStatement('SELECT "a\\";SELECT 1', 'postgres')).toThrow(
|
||||
/one SQL statement/
|
||||
)
|
||||
})
|
||||
|
||||
it('treats postgres E-strings as backslash-escaped (single statement)', () => {
|
||||
// E'it\'s; fine' is one literal: \' is an escaped quote, so the ;
|
||||
// stays inside the string and this is a single statement.
|
||||
|
||||
Reference in New Issue
Block a user