Replace magic strings with AuditAction/AuditStatus enums across all consumers, add logAuditWithCurrentUser() convenience wrapper, extend audit coverage to data import, result export, app update, and ERP credentials operations, and harden crash handlers with try/catch to prevent audit failures from cascading. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
579 lines
20 KiB
TypeScript
579 lines
20 KiB
TypeScript
/**
|
|
* Unit tests for PostgreSqlService
|
|
* Covers both unconnected state and connected-path operations using mocked pg driver.
|
|
* Also tests the prepareSql pure function (no mocks needed for those).
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
import { PostgreSqlService, prepareSql } from '@main/services/database/postgresql'
|
|
|
|
// ---- Hoisted mock functions ----
|
|
const { mockPgPool, mockPgClient } = vi.hoisted(() => {
|
|
const client = {
|
|
query: vi.fn(),
|
|
release: vi.fn()
|
|
}
|
|
const pool = {
|
|
connect: vi.fn(() => client),
|
|
query: vi.fn(),
|
|
end: vi.fn()
|
|
}
|
|
return { mockPgPool: pool, mockPgClient: client }
|
|
})
|
|
|
|
// Mock pg driver (must use regular function because source uses `new Pool(...)`)
|
|
vi.mock('pg', () => ({
|
|
Pool: vi.fn(function () {
|
|
return mockPgPool
|
|
})
|
|
}))
|
|
|
|
// Mock logger
|
|
vi.mock('@main/services/logger', () => ({
|
|
createLogger: () => ({
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
debug: vi.fn()
|
|
}),
|
|
trackDuration: async <T>(fn: () => Promise<T>) => {
|
|
const result = await fn()
|
|
return { result }
|
|
}
|
|
}))
|
|
|
|
const mockConfig = {
|
|
host: 'localhost',
|
|
port: 5432,
|
|
user: 'test',
|
|
password: 'test',
|
|
database: 'testdb'
|
|
}
|
|
|
|
describe('PostgreSqlService Unit Tests', () => {
|
|
let service: PostgreSqlService
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
service = new PostgreSqlService(mockConfig)
|
|
mockPgPool.connect.mockResolvedValue(mockPgClient)
|
|
mockPgPool.query.mockResolvedValue({ rows: [], fields: [], rowCount: 0 })
|
|
mockPgPool.end.mockResolvedValue(undefined)
|
|
mockPgClient.query.mockResolvedValue({ rows: [] })
|
|
mockPgClient.release.mockReturnValue(undefined)
|
|
})
|
|
|
|
describe('constructor', () => {
|
|
it('should create service with config', () => {
|
|
expect(service).toBeDefined()
|
|
expect(service.isConnected()).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('type', () => {
|
|
it('should return postgresql', () => {
|
|
expect(service.type).toBe('postgresql')
|
|
})
|
|
})
|
|
|
|
describe('isConnected', () => {
|
|
it('should return false when not connected', () => {
|
|
expect(service.isConnected()).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('connect', () => {
|
|
it('should throw error when pool creation fails', async () => {
|
|
mockPgPool.connect.mockRejectedValue(new Error('connection refused'))
|
|
const svc = new PostgreSqlService(mockConfig)
|
|
await expect(svc.connect()).rejects.toThrow('Failed to connect to PostgreSQL')
|
|
})
|
|
|
|
it('should establish connection and release test client', async () => {
|
|
await service.connect()
|
|
expect(mockPgPool.connect).toHaveBeenCalled()
|
|
expect(mockPgClient.release).toHaveBeenCalled()
|
|
expect(service.isConnected()).toBe(true)
|
|
})
|
|
|
|
it('should throw when already connected', async () => {
|
|
await service.connect()
|
|
await expect(service.connect()).rejects.toThrow('Already connected to PostgreSQL')
|
|
})
|
|
|
|
it('should reset pool to null on connection failure', async () => {
|
|
mockPgPool.connect.mockRejectedValue(new Error('timeout'))
|
|
await expect(service.connect()).rejects.toThrow('Failed to connect to PostgreSQL')
|
|
expect(service.isConnected()).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('query', () => {
|
|
it('should throw error when not connected', async () => {
|
|
await expect(service.query('SELECT 1')).rejects.toThrow('Not connected to PostgreSQL')
|
|
})
|
|
|
|
it('should execute query and return rows with columns', async () => {
|
|
await service.connect()
|
|
mockPgPool.query.mockResolvedValue({
|
|
rows: [{ ID: 1, Name: 'test' }],
|
|
fields: [{ name: 'ID' }, { name: 'Name' }],
|
|
rowCount: 1
|
|
})
|
|
|
|
const result = await service.query('SELECT ID, Name FROM Users')
|
|
|
|
expect(result.rows).toEqual([{ ID: 1, Name: 'test' }])
|
|
expect(result.columns).toEqual(['ID', 'Name'])
|
|
expect(result.rowCount).toBe(1)
|
|
})
|
|
|
|
it('should pass params through to pool.query', async () => {
|
|
await service.connect()
|
|
mockPgPool.query.mockResolvedValue({ rows: [], fields: [], rowCount: 0 })
|
|
|
|
await service.query('SELECT * FROM Users WHERE ID = $1', [42])
|
|
|
|
expect(mockPgPool.query).toHaveBeenCalledWith(expect.any(String), [42])
|
|
})
|
|
|
|
it('should fallback to rows.length when rowCount is null', async () => {
|
|
await service.connect()
|
|
mockPgPool.query.mockResolvedValue({
|
|
rows: [{ ID: 1 }, { ID: 2 }],
|
|
fields: [{ name: 'ID' }],
|
|
rowCount: null
|
|
})
|
|
|
|
const result = await service.query('SELECT ID FROM Users')
|
|
expect(result.rowCount).toBe(2)
|
|
})
|
|
|
|
it('should wrap query errors with context', async () => {
|
|
await service.connect()
|
|
mockPgPool.query.mockRejectedValue(new Error('syntax error'))
|
|
|
|
await expect(service.query('INVALID SQL')).rejects.toThrow('PostgreSQL query failed')
|
|
})
|
|
})
|
|
|
|
describe('transaction', () => {
|
|
it('should throw error when not connected', async () => {
|
|
await expect(service.transaction([{ sql: 'SELECT 1' }])).rejects.toThrow(
|
|
'Not connected to PostgreSQL'
|
|
)
|
|
})
|
|
|
|
it('should execute all queries within BEGIN/COMMIT and release client', async () => {
|
|
await service.connect()
|
|
mockPgClient.query.mockResolvedValue({ rows: [] })
|
|
|
|
await service.transaction([{ sql: 'SELECT 1', params: [1] }, { sql: 'SELECT 2' }])
|
|
|
|
// BEGIN + 2 queries + COMMIT
|
|
expect(mockPgClient.query).toHaveBeenCalledTimes(4)
|
|
expect(mockPgClient.query).toHaveBeenNthCalledWith(1, 'BEGIN')
|
|
expect(mockPgClient.query).toHaveBeenNthCalledWith(4, 'COMMIT')
|
|
expect(mockPgClient.release).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should rollback and release client on query failure', async () => {
|
|
await service.connect()
|
|
mockPgClient.query
|
|
.mockResolvedValueOnce({ rows: [] }) // BEGIN
|
|
.mockRejectedValueOnce(new Error('constraint violation')) // query fails
|
|
.mockResolvedValueOnce({ rows: [] }) // ROLLBACK
|
|
|
|
await expect(service.transaction([{ sql: 'SELECT 1', params: [1] }])).rejects.toThrow(
|
|
'PostgreSQL transaction failed'
|
|
)
|
|
|
|
expect(mockPgClient.query).toHaveBeenCalledWith('ROLLBACK')
|
|
expect(mockPgClient.release).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('disconnect', () => {
|
|
it('should resolve when not connected', async () => {
|
|
await expect(service.disconnect()).resolves.not.toThrow()
|
|
})
|
|
|
|
it('should end pool and reset state', async () => {
|
|
await service.connect()
|
|
expect(service.isConnected()).toBe(true)
|
|
|
|
await service.disconnect()
|
|
|
|
expect(mockPgPool.end).toHaveBeenCalled()
|
|
expect(service.isConnected()).toBe(false)
|
|
})
|
|
|
|
it('should wrap disconnect errors', async () => {
|
|
await service.connect()
|
|
mockPgPool.end.mockRejectedValue(new Error('pool end failed'))
|
|
|
|
await expect(service.disconnect()).rejects.toThrow('Failed to disconnect from PostgreSQL')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('prepareSql', () => {
|
|
it('should quote unquoted column names in SELECT', () => {
|
|
const sql = 'SELECT ID, UserName, UserType FROM "dbo"."BIPUsers"'
|
|
const result = prepareSql(sql)
|
|
expect(result).toBe('SELECT "ID", "UserName", "UserType" FROM "dbo"."BIPUsers"')
|
|
})
|
|
|
|
it('should quote column names in WHERE clause', () => {
|
|
const sql = 'WHERE UserName = $1 AND Password = $2'
|
|
const result = prepareSql(sql)
|
|
expect(result).toBe('WHERE "UserName" = $1 AND "Password" = $2')
|
|
})
|
|
|
|
it('should quote column names in INSERT', () => {
|
|
const sql = 'INSERT INTO "dbo"."BIPUsers" (UserName, Password, UserType) VALUES ($1, $2, $3)'
|
|
const result = prepareSql(sql)
|
|
expect(result).toBe(
|
|
'INSERT INTO "dbo"."BIPUsers" ("UserName", "Password", "UserType") VALUES ($1, $2, $3)'
|
|
)
|
|
})
|
|
|
|
it('should quote column names in UPDATE SET', () => {
|
|
const sql = 'UPDATE "dbo"."BIPUsers" SET UserType = $1 WHERE UserName = $2'
|
|
const result = prepareSql(sql)
|
|
expect(result).toBe('UPDATE "dbo"."BIPUsers" SET "UserType" = $1 WHERE "UserName" = $2')
|
|
})
|
|
|
|
it('should quote column names in DELETE', () => {
|
|
const sql = 'DELETE FROM "dbo"."BIPUsers" WHERE UserName = $1'
|
|
const result = prepareSql(sql)
|
|
expect(result).toBe('DELETE FROM "dbo"."BIPUsers" WHERE "UserName" = $1')
|
|
})
|
|
|
|
it('should quote column names in ORDER BY', () => {
|
|
const sql = 'SELECT UserName FROM "dbo"."BIPUsers" ORDER BY UserName'
|
|
const result = prepareSql(sql)
|
|
expect(result).toBe('SELECT "UserName" FROM "dbo"."BIPUsers" ORDER BY "UserName"')
|
|
})
|
|
|
|
it('should not quote SQL keywords', () => {
|
|
const sql = 'SELECT ID FROM "dbo"."BIPUsers" WHERE UserName = $1'
|
|
const result = prepareSql(sql)
|
|
expect(result).not.toContain('"SELECT"')
|
|
expect(result).not.toContain('"FROM"')
|
|
expect(result).not.toContain('"WHERE"')
|
|
expect(result).not.toContain('"AND"')
|
|
})
|
|
|
|
it('should not quote already-quoted identifiers', () => {
|
|
const sql = 'SELECT "ID" FROM "dbo"."BIPUsers"'
|
|
const result = prepareSql(sql)
|
|
expect(result).toBe('SELECT "ID" FROM "dbo"."BIPUsers"')
|
|
})
|
|
|
|
it('should preserve string literals', () => {
|
|
const sql = "WHERE Status = 'active'"
|
|
const result = prepareSql(sql)
|
|
expect(result).toBe('WHERE "Status" = \'active\'')
|
|
})
|
|
|
|
it('should preserve string literals with escaped quotes', () => {
|
|
const sql = "WHERE UserName = 'O''Brien'"
|
|
const result = prepareSql(sql)
|
|
expect(result).toBe("WHERE \"UserName\" = 'O''Brien'")
|
|
})
|
|
|
|
it('should preserve $N parameter placeholders', () => {
|
|
const sql = 'WHERE UserName = $1 AND Password = $2'
|
|
const result = prepareSql(sql)
|
|
expect(result).toContain('$1')
|
|
expect(result).toContain('$2')
|
|
})
|
|
|
|
it('should handle COUNT(*) correctly', () => {
|
|
const sql = 'SELECT COUNT(*) as count FROM "dbo"."BIPUsers" WHERE UserName = $1'
|
|
const result = prepareSql(sql)
|
|
expect(result).toBe('SELECT COUNT(*) as count FROM "dbo"."BIPUsers" WHERE "UserName" = $1')
|
|
})
|
|
|
|
it('should quote underscore-containing column names', () => {
|
|
const sql = 'SELECT ERP_URL, ERP_Username, ERP_Password FROM "dbo"."BIPUsers"'
|
|
const result = prepareSql(sql)
|
|
expect(result).toBe('SELECT "ERP_URL", "ERP_Username", "ERP_Password" FROM "dbo"."BIPUsers"')
|
|
})
|
|
|
|
it('should handle ON CONFLICT DO UPDATE SET with EXCLUDED', () => {
|
|
const sql =
|
|
'INSERT INTO "dbo"."Materials" (MaterialCode, ManagerName) VALUES ($1, $2) ON CONFLICT ("MaterialCode") DO UPDATE SET "ManagerName" = EXCLUDED."ManagerName"'
|
|
const result = prepareSql(sql)
|
|
expect(result).toBe(
|
|
'INSERT INTO "dbo"."Materials" ("MaterialCode", "ManagerName") VALUES ($1, $2) ON CONFLICT ("MaterialCode") DO UPDATE SET "ManagerName" = EXCLUDED."ManagerName"'
|
|
)
|
|
})
|
|
|
|
it('should handle CURRENT_TIMESTAMP without quoting', () => {
|
|
const sql = 'INSERT INTO t (OperationTime) VALUES (CURRENT_TIMESTAMP)'
|
|
const result = prepareSql(sql)
|
|
expect(result).toBe('INSERT INTO "t" ("OperationTime") VALUES (CURRENT_TIMESTAMP)')
|
|
})
|
|
|
|
it('should handle LIMIT OFFSET without quoting', () => {
|
|
const sql = 'SELECT UserName FROM "dbo"."BIPUsers" LIMIT 10 OFFSET 20'
|
|
const result = prepareSql(sql)
|
|
expect(result).toBe('SELECT "UserName" FROM "dbo"."BIPUsers" LIMIT 10 OFFSET 20')
|
|
})
|
|
|
|
it('should return empty string for empty input', () => {
|
|
expect(prepareSql('')).toBe('')
|
|
})
|
|
|
|
it('should handle full BIPUsersDAO authenticate query', () => {
|
|
const sql = `
|
|
SELECT ID, UserName, UserType
|
|
FROM "dbo"."BIPUsers"
|
|
WHERE UserName = $1 AND Password = $2
|
|
`
|
|
const result = prepareSql(sql)
|
|
expect(result).toContain('"ID"')
|
|
expect(result).toContain('"UserName"')
|
|
expect(result).toContain('"UserType"')
|
|
expect(result).toContain('"Password"')
|
|
expect(result).toContain('$1')
|
|
expect(result).toContain('$2')
|
|
expect(result).toContain('"dbo"."BIPUsers"')
|
|
})
|
|
|
|
it('should handle full BIPUsersDAO userExists query', () => {
|
|
const sql = `
|
|
SELECT COUNT(*) as count
|
|
FROM "dbo"."BIPUsers"
|
|
WHERE UserName = $1
|
|
`
|
|
const result = prepareSql(sql)
|
|
expect(result).toContain('COUNT(*)')
|
|
expect(result).toContain('as count')
|
|
expect(result).toContain('"UserName"')
|
|
})
|
|
|
|
// ==================== Window Functions ====================
|
|
|
|
it('should handle ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)', () => {
|
|
const sql = `
|
|
SELECT UserName, ROW_NUMBER() OVER (PARTITION BY UserType ORDER BY CreatedAt DESC) as rn
|
|
FROM "dbo"."BIPUsers"
|
|
`
|
|
const result = prepareSql(sql)
|
|
expect(result).toContain('"UserName"')
|
|
expect(result).toContain('"UserType"')
|
|
expect(result).toContain('"CreatedAt"')
|
|
expect(result).not.toContain('"ROW_NUMBER"')
|
|
expect(result).not.toContain('"OVER"')
|
|
expect(result).not.toContain('"PARTITION"')
|
|
expect(result).not.toContain('"ORDER"')
|
|
})
|
|
|
|
it('should handle RANK() and DENSE_RANK()', () => {
|
|
const sql = `
|
|
SELECT MaterialCode, RANK() OVER (ORDER BY Quantity DESC) as rnk, DENSE_RANK() OVER (ORDER BY Quantity DESC) as drnk
|
|
FROM "dbo"."Materials"
|
|
`
|
|
const result = prepareSql(sql)
|
|
expect(result).toContain('"MaterialCode"')
|
|
expect(result).toContain('"Quantity"')
|
|
expect(result).not.toContain('"RANK"')
|
|
expect(result).not.toContain('"DENSE_RANK"')
|
|
})
|
|
|
|
it('should handle LAG() and LEAD()', () => {
|
|
const sql = `
|
|
SELECT OrderId, LAG(TotalAmount, 1) OVER (ORDER BY OrderDate) as prevAmount, LEAD(TotalAmount, 1) OVER (ORDER BY OrderDate) as nextAmount
|
|
FROM "dbo"."Orders"
|
|
`
|
|
const result = prepareSql(sql)
|
|
expect(result).toContain('"OrderId"')
|
|
expect(result).toContain('"TotalAmount"')
|
|
expect(result).toContain('"OrderDate"')
|
|
expect(result).not.toContain('"LAG"')
|
|
expect(result).not.toContain('"LEAD"')
|
|
})
|
|
|
|
// ==================== CTEs (Common Table Expressions) ====================
|
|
|
|
it('should handle WITH clause', () => {
|
|
const sql = `
|
|
WITH UserSummary AS (
|
|
SELECT UserId, COUNT(OrderId) as OrderCount
|
|
FROM "dbo"."Orders"
|
|
GROUP BY UserId
|
|
)
|
|
SELECT UserName, OrderCount
|
|
FROM UserSummary
|
|
JOIN "dbo"."BIPUsers" ON UserSummary.UserId = "dbo"."BIPUsers".ID
|
|
`
|
|
const result = prepareSql(sql)
|
|
expect(result).toContain('"UserId"')
|
|
expect(result).toContain('"OrderId"')
|
|
expect(result).toContain('"UserName"')
|
|
expect(result).not.toContain('"WITH"')
|
|
expect(result).not.toContain('"AS"')
|
|
expect(result).not.toContain('"FROM"')
|
|
expect(result).not.toContain('"JOIN"')
|
|
expect(result).not.toContain('"ON"')
|
|
})
|
|
|
|
it('should handle recursive CTE', () => {
|
|
const sql = `
|
|
WITH RECURSIVE CategoryTree AS (
|
|
SELECT CategoryId, ParentCategoryId, CategoryName, 0 as Level
|
|
FROM "dbo"."Categories"
|
|
WHERE ParentCategoryId IS NULL
|
|
UNION ALL
|
|
SELECT c.CategoryId, c.ParentCategoryId, c.CategoryName, ct.Level + 1
|
|
FROM "dbo"."Categories" c
|
|
INNER JOIN CategoryTree ct ON c.ParentCategoryId = ct.CategoryId
|
|
)
|
|
SELECT * FROM CategoryTree
|
|
`
|
|
const result = prepareSql(sql)
|
|
expect(result).toContain('"CategoryId"')
|
|
expect(result).toContain('"ParentCategoryId"')
|
|
expect(result).toContain('"CategoryName"')
|
|
expect(result).not.toContain('"WITH"')
|
|
expect(result).not.toContain('"RECURSIVE"')
|
|
expect(result).not.toContain('"UNION"')
|
|
expect(result).not.toContain('"ALL"')
|
|
expect(result).not.toContain('"INNER"')
|
|
expect(result).not.toContain('"JOIN"')
|
|
})
|
|
|
|
// ==================== Advanced Grouping ====================
|
|
|
|
it('should handle ROLLUP', () => {
|
|
const sql = `
|
|
SELECT DepartmentId, JobTitle, COUNT(*) as EmployeeCount
|
|
FROM "dbo"."Employees"
|
|
GROUP BY ROLLUP (DepartmentId, JobTitle)
|
|
`
|
|
const result = prepareSql(sql)
|
|
expect(result).toContain('"DepartmentId"')
|
|
expect(result).toContain('"JobTitle"')
|
|
expect(result).not.toContain('"GROUP"')
|
|
expect(result).not.toContain('"BY"')
|
|
expect(result).not.toContain('"ROLLUP"')
|
|
})
|
|
|
|
it('should handle CUBE', () => {
|
|
const sql = `
|
|
SELECT Year, Quarter, Region, SUM(SalesAmount) as TotalSales
|
|
FROM "dbo"."Sales"
|
|
GROUP BY CUBE (Year, Quarter, Region)
|
|
`
|
|
const result = prepareSql(sql)
|
|
expect(result).toContain('"Year"')
|
|
expect(result).toContain('"Quarter"')
|
|
expect(result).toContain('"Region"')
|
|
expect(result).toContain('"SalesAmount"')
|
|
expect(result).not.toContain('"CUBE"')
|
|
expect(result).not.toContain('"GROUP"')
|
|
expect(result).not.toContain('"BY"')
|
|
})
|
|
|
|
it('should handle GROUPING SETS', () => {
|
|
const sql = `
|
|
SELECT DepartmentId, JobTitle, COUNT(*) as EmployeeCount
|
|
FROM "dbo"."Employees"
|
|
GROUP BY GROUPING SETS ((DepartmentId, JobTitle), (DepartmentId), ())
|
|
`
|
|
const result = prepareSql(sql)
|
|
expect(result).toContain('"DepartmentId"')
|
|
expect(result).toContain('"JobTitle"')
|
|
expect(result).not.toContain('"GROUPING"')
|
|
expect(result).not.toContain('"SETS"')
|
|
expect(result).not.toContain('"GROUP"')
|
|
expect(result).not.toContain('"BY"')
|
|
})
|
|
|
|
// ==================== CASE Expressions ====================
|
|
|
|
it('should handle simple CASE', () => {
|
|
const sql = `
|
|
SELECT UserName, CASE UserType
|
|
WHEN 'admin' THEN 'Administrator'
|
|
WHEN 'user' THEN 'Regular User'
|
|
ELSE 'Guest'
|
|
END as UserRole
|
|
FROM "dbo"."BIPUsers"
|
|
`
|
|
const result = prepareSql(sql)
|
|
expect(result).toContain('"UserName"')
|
|
expect(result).toContain('"UserType"')
|
|
expect(result).not.toContain('"CASE"')
|
|
expect(result).not.toContain('"WHEN"')
|
|
expect(result).not.toContain('"THEN"')
|
|
expect(result).not.toContain('"ELSE"')
|
|
expect(result).not.toContain('"END"')
|
|
})
|
|
|
|
it('should handle searched CASE', () => {
|
|
const sql = `
|
|
SELECT OrderId, TotalAmount,
|
|
CASE
|
|
WHEN TotalAmount > 10000 THEN 'Large'
|
|
WHEN TotalAmount > 1000 THEN 'Medium'
|
|
ELSE 'Small'
|
|
END as OrderSize
|
|
FROM "dbo"."Orders"
|
|
`
|
|
const result = prepareSql(sql)
|
|
expect(result).toContain('"OrderId"')
|
|
expect(result).toContain('"TotalAmount"')
|
|
expect(result).not.toContain('"CASE"')
|
|
expect(result).not.toContain('"WHEN"')
|
|
expect(result).not.toContain('"THEN"')
|
|
expect(result).not.toContain('"ELSE"')
|
|
expect(result).not.toContain('"END"')
|
|
})
|
|
|
|
// ==================== Set Operations ====================
|
|
|
|
it('should handle UNION, UNION ALL, INTERSECT, EXCEPT', () => {
|
|
const sql = `
|
|
SELECT UserId FROM "dbo"."ActiveUsers"
|
|
UNION
|
|
SELECT UserId FROM "dbo"."PremiumUsers"
|
|
UNION ALL
|
|
SELECT UserId FROM "dbo"."TrialUsers"
|
|
INTERSECT
|
|
SELECT UserId FROM "dbo"."VerifiedUsers"
|
|
EXCEPT
|
|
SELECT UserId FROM "dbo"."BannedUsers"
|
|
`
|
|
const result = prepareSql(sql)
|
|
expect(result).toContain('"UserId"')
|
|
expect(result).not.toContain('"UNION"')
|
|
expect(result).not.toContain('"ALL"')
|
|
expect(result).not.toContain('"INTERSECT"')
|
|
expect(result).not.toContain('"EXCEPT"')
|
|
expect(result).not.toContain('"SELECT"')
|
|
expect(result).not.toContain('"FROM"')
|
|
})
|
|
|
|
// ==================== JSON Operators ====================
|
|
|
|
it('should handle -> and ->> operators', () => {
|
|
const sql = `
|
|
SELECT UserId, ProfileData->'address'->>'city' as City, ProfileData->'contact'->>'phone' as Phone
|
|
FROM "dbo"."Users"
|
|
WHERE ProfileData->'preferences'->>'newsletter' = 'true'
|
|
`
|
|
const result = prepareSql(sql)
|
|
expect(result).toContain('"UserId"')
|
|
expect(result).toContain('"ProfileData"')
|
|
expect(result).toContain('->')
|
|
expect(result).toContain('->>')
|
|
expect(result).not.toContain('"SELECT"')
|
|
expect(result).not.toContain('"FROM"')
|
|
expect(result).not.toContain('"WHERE"')
|
|
})
|
|
})
|