The prepareSql function quotes any word not in SQL_KEYWORDS as an identifier. Since AT and ZONE were missing from the set, the expression (NOW() AT TIME ZONE 'UTC') was mangled into (NOW() "AT" TIME "ZONE" 'UTC'), causing INSERT failures on PostgreSQL. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
146 lines
4.4 KiB
TypeScript
146 lines
4.4 KiB
TypeScript
/**
|
|
* Unit tests for PostgreSqlDialect
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest'
|
|
import { PostgreSqlDialect } from '@services/database/dialects/postgresql-dialect'
|
|
|
|
describe('PostgreSqlDialect', () => {
|
|
const dialect = new PostgreSqlDialect()
|
|
|
|
describe('dbType', () => {
|
|
it('should return postgresql', () => {
|
|
expect(dialect.dbType).toBe('postgresql')
|
|
})
|
|
})
|
|
|
|
describe('quoteTableName', () => {
|
|
it('should wrap schema and table in double quotes', () => {
|
|
expect(dialect.quoteTableName('dbo', 'Table')).toBe('"dbo"."Table"')
|
|
})
|
|
|
|
it('should handle arbitrary names', () => {
|
|
expect(dialect.quoteTableName('my_schema', 'my_table')).toBe('"my_schema"."my_table"')
|
|
})
|
|
})
|
|
|
|
describe('param', () => {
|
|
it('should return $N with 1-based index ($1 for param(0))', () => {
|
|
expect(dialect.param(0)).toBe('$1')
|
|
expect(dialect.param(1)).toBe('$2')
|
|
expect(dialect.param(3)).toBe('$4')
|
|
expect(dialect.param(10)).toBe('$11')
|
|
})
|
|
})
|
|
|
|
describe('params', () => {
|
|
it('should return comma-separated $N placeholders (1-based)', () => {
|
|
expect(dialect.params(1)).toBe('$1')
|
|
expect(dialect.params(3)).toBe('$1,$2,$3')
|
|
expect(dialect.params(5)).toBe('$1,$2,$3,$4,$5')
|
|
})
|
|
|
|
it('should return empty string for count 0', () => {
|
|
expect(dialect.params(0)).toBe('')
|
|
})
|
|
})
|
|
|
|
describe('currentTimestamp', () => {
|
|
it('should return explicit UTC timestamp expression', () => {
|
|
expect(dialect.currentTimestamp()).toBe("(NOW() AT TIME ZONE 'UTC')")
|
|
})
|
|
})
|
|
|
|
describe('upsert', () => {
|
|
it('should generate ON CONFLICT DO UPDATE SQL', () => {
|
|
const result = dialect.upsert({
|
|
table: '"dbo"."Table"',
|
|
keyColumns: ['id'],
|
|
allColumns: ['id', 'name', 'value'],
|
|
startParamIndex: 0
|
|
})
|
|
|
|
expect(result.sql).toContain('INSERT INTO "dbo"."Table" (id, name, value)')
|
|
expect(result.sql).toContain('VALUES ($1, $2, $3)')
|
|
expect(result.sql).toContain('ON CONFLICT ("id")')
|
|
expect(result.sql).toContain('DO UPDATE SET')
|
|
expect(result.sql).toContain('"name" = EXCLUDED."name"')
|
|
expect(result.sql).toContain('"value" = EXCLUDED."value"')
|
|
// key column should NOT appear in DO UPDATE SET
|
|
expect(result.sql).not.toContain('"id" = EXCLUDED."id"')
|
|
expect(result.nextParamIndex).toBe(3)
|
|
})
|
|
|
|
it('should handle composite key columns with all double-quoted', () => {
|
|
const result = dialect.upsert({
|
|
table: '"dbo"."Table"',
|
|
keyColumns: ['id', 'code'],
|
|
allColumns: ['id', 'code', 'name'],
|
|
startParamIndex: 0
|
|
})
|
|
|
|
expect(result.sql).toContain('ON CONFLICT ("id", "code")')
|
|
expect(result.sql).toContain('"name" = EXCLUDED."name"')
|
|
expect(result.nextParamIndex).toBe(3)
|
|
})
|
|
|
|
it('should use startParamIndex for parameter numbering', () => {
|
|
const result = dialect.upsert({
|
|
table: '"dbo"."Table"',
|
|
keyColumns: ['id'],
|
|
allColumns: ['id', 'name'],
|
|
startParamIndex: 5
|
|
})
|
|
|
|
// startParamIndex=5 means $6, $7 (1-based: index+1)
|
|
expect(result.sql).toContain('$6')
|
|
expect(result.sql).toContain('$7')
|
|
expect(result.nextParamIndex).toBe(7)
|
|
})
|
|
})
|
|
|
|
describe('paginate', () => {
|
|
it('should append LIMIT and OFFSET with literal values', () => {
|
|
const result = dialect.paginate({
|
|
sql: 'SELECT * FROM "dbo"."Table"',
|
|
limit: 10,
|
|
offset: 20,
|
|
paramIndex: 0
|
|
})
|
|
|
|
expect(result.sql).toBe('SELECT * FROM "dbo"."Table" LIMIT 10 OFFSET 20')
|
|
expect(result.nextParamIndex).toBe(0)
|
|
})
|
|
|
|
it('should use 0 as default offset', () => {
|
|
const result = dialect.paginate({
|
|
sql: 'SELECT * FROM "dbo"."Table"',
|
|
limit: 50,
|
|
paramIndex: 3
|
|
})
|
|
|
|
expect(result.sql).toBe('SELECT * FROM "dbo"."Table" LIMIT 50 OFFSET 0')
|
|
expect(result.nextParamIndex).toBe(3)
|
|
})
|
|
|
|
it('should not change nextParamIndex (no params added)', () => {
|
|
const result = dialect.paginate({
|
|
sql: 'SELECT * FROM "dbo"."Table"',
|
|
limit: 100,
|
|
offset: 50,
|
|
paramIndex: 10
|
|
})
|
|
|
|
expect(result.nextParamIndex).toBe(10)
|
|
})
|
|
})
|
|
|
|
describe('maxBatchRows', () => {
|
|
it('should return 1000 regardless of columns', () => {
|
|
expect(dialect.maxBatchRows(1)).toBe(1000)
|
|
expect(dialect.maxBatchRows(10)).toBe(1000)
|
|
expect(dialect.maxBatchRows(100)).toBe(1000)
|
|
})
|
|
})
|
|
})
|