fix: support postgres material upserts without unique constraints
This commit is contained in:
@@ -110,6 +110,58 @@ export class MaterialsToBeDeletedDAO {
|
||||
const manager = managerName?.trim() || null
|
||||
const dialect = this.getDialect()
|
||||
|
||||
if (dbService.type === 'postgresql') {
|
||||
const updateSql = `
|
||||
UPDATE ${tableName}
|
||||
SET ManagerName = ${dialect.param(0)}
|
||||
WHERE MaterialCode = ${dialect.param(1)}
|
||||
`
|
||||
|
||||
const updateResult = await trackDuration(
|
||||
async () => await dbService.query(updateSql, [manager, code]),
|
||||
{
|
||||
operationName: 'MaterialsToBeDeletedDAO.upsertMaterial',
|
||||
context: { tableName, operationType: 'UPSERT_UPDATE_FIRST' }
|
||||
}
|
||||
)
|
||||
|
||||
if (updateResult.result.rowCount > 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
const insertSql = `
|
||||
INSERT INTO ${tableName} (MaterialCode, ManagerName)
|
||||
SELECT ${dialect.param(0)}, ${dialect.param(1)}
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM ${tableName}
|
||||
WHERE MaterialCode = ${dialect.param(0)}
|
||||
)
|
||||
`
|
||||
|
||||
const insertResult = await trackDuration(
|
||||
async () => await dbService.query(insertSql, [code, manager]),
|
||||
{
|
||||
operationName: 'MaterialsToBeDeletedDAO.upsertMaterial',
|
||||
context: { tableName, operationType: 'UPSERT_INSERT_FALLBACK' }
|
||||
}
|
||||
)
|
||||
|
||||
if (insertResult.result.rowCount > 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
const retryUpdateResult = await trackDuration(
|
||||
async () => await dbService.query(updateSql, [manager, code]),
|
||||
{
|
||||
operationName: 'MaterialsToBeDeletedDAO.upsertMaterial',
|
||||
context: { tableName, operationType: 'UPSERT_UPDATE_RETRY' }
|
||||
}
|
||||
)
|
||||
|
||||
return retryUpdateResult.result.rowCount > 0
|
||||
}
|
||||
|
||||
const { sql: sqlString } = dialect.upsert({
|
||||
table: tableName,
|
||||
keyColumns: ['MaterialCode'],
|
||||
@@ -177,22 +229,12 @@ export class MaterialsToBeDeletedDAO {
|
||||
}
|
||||
|
||||
try {
|
||||
const { sql: sqlString } = dialect.upsert({
|
||||
table: tableName,
|
||||
keyColumns: ['MaterialCode'],
|
||||
allColumns: ['MaterialCode', 'ManagerName'],
|
||||
startParamIndex: 0
|
||||
})
|
||||
|
||||
await trackDuration(
|
||||
async () => await dbService.query(sqlString, [materialCode, managerName || null]),
|
||||
{
|
||||
operationName: 'MaterialsToBeDeletedDAO.upsertBatch',
|
||||
context: { tableName, operationType: 'UPSERT', batchId }
|
||||
}
|
||||
)
|
||||
|
||||
const success = await this.upsertMaterial(materialCode, managerName)
|
||||
if (success) {
|
||||
stats.success++
|
||||
} else {
|
||||
stats.failed++
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('Error upserting material', {
|
||||
tableName,
|
||||
@@ -201,7 +243,6 @@ export class MaterialsToBeDeletedDAO {
|
||||
materialCode,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
stats.failed++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,19 +279,8 @@ export class MaterialsToBeDeletedDAO {
|
||||
managerName: string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const dbService = await this.getDatabaseService()
|
||||
const tableName = this.getTableName()
|
||||
const dialect = this.getDialect()
|
||||
|
||||
const { sql: sqlString } = dialect.upsert({
|
||||
table: tableName,
|
||||
keyColumns: ['MaterialCode'],
|
||||
allColumns: ['MaterialCode', 'ManagerName'],
|
||||
startParamIndex: 0
|
||||
})
|
||||
await dbService.query(sqlString, [materialCode, managerName || null])
|
||||
|
||||
return { success: true }
|
||||
const success = await this.upsertMaterial(materialCode, managerName)
|
||||
return { success }
|
||||
} catch (error) {
|
||||
log.error('Update manager error', {
|
||||
materialCode,
|
||||
|
||||
@@ -210,6 +210,58 @@ export class MaterialsTypeToBeDeletedDAO {
|
||||
const manager = managerName?.trim() || null
|
||||
const dialect = this.getDialect()
|
||||
|
||||
if (dbService.type === 'postgresql') {
|
||||
const updateSql = `
|
||||
UPDATE ${tableName}
|
||||
SET ManagerName = ${dialect.param(0)}
|
||||
WHERE MaterialName = ${dialect.param(1)}
|
||||
`
|
||||
|
||||
const updateResult = await trackDuration(
|
||||
async () => await dbService.query(updateSql, [manager, name]),
|
||||
{
|
||||
operationName: 'MaterialsTypeToBeDeletedDAO.upsertMaterial',
|
||||
context: { tableName, operationType: 'UPSERT_UPDATE_FIRST' }
|
||||
}
|
||||
)
|
||||
|
||||
if (updateResult.result.rowCount > 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
const insertSql = `
|
||||
INSERT INTO ${tableName} (MaterialName, ManagerName)
|
||||
SELECT ${dialect.param(0)}, ${dialect.param(1)}
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM ${tableName}
|
||||
WHERE MaterialName = ${dialect.param(0)}
|
||||
)
|
||||
`
|
||||
|
||||
const insertResult = await trackDuration(
|
||||
async () => await dbService.query(insertSql, [name, manager]),
|
||||
{
|
||||
operationName: 'MaterialsTypeToBeDeletedDAO.upsertMaterial',
|
||||
context: { tableName, operationType: 'UPSERT_INSERT_FALLBACK' }
|
||||
}
|
||||
)
|
||||
|
||||
if (insertResult.result.rowCount > 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
const retryUpdateResult = await trackDuration(
|
||||
async () => await dbService.query(updateSql, [manager, name]),
|
||||
{
|
||||
operationName: 'MaterialsTypeToBeDeletedDAO.upsertMaterial',
|
||||
context: { tableName, operationType: 'UPSERT_UPDATE_RETRY' }
|
||||
}
|
||||
)
|
||||
|
||||
return retryUpdateResult.result.rowCount > 0
|
||||
}
|
||||
|
||||
const { sql: sqlString } = dialect.upsert({
|
||||
table: tableName,
|
||||
keyColumns: ['MaterialName'],
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const queryMock = vi.fn()
|
||||
const createMock = vi.fn()
|
||||
const trackDurationMock = vi.fn(async (fn: () => Promise<unknown>) => ({ result: await fn() }))
|
||||
|
||||
vi.mock('../../../../src/main/services/logger', () => ({
|
||||
createLogger: () => ({
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn()
|
||||
}),
|
||||
getRequestId: () => 'test-request-id',
|
||||
trackDuration: trackDurationMock
|
||||
}))
|
||||
|
||||
vi.mock('../../../../src/main/services/database/index', () => ({
|
||||
create: createMock
|
||||
}))
|
||||
|
||||
describe('MaterialsToBeDeletedDAO (PostgreSQL compatibility)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
createMock.mockResolvedValue({
|
||||
type: 'postgresql',
|
||||
isConnected: () => true,
|
||||
query: queryMock,
|
||||
disconnect: vi.fn()
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to update-then-insert instead of ON CONFLICT for PostgreSQL inserts', async () => {
|
||||
queryMock
|
||||
.mockResolvedValueOnce({
|
||||
rows: [],
|
||||
columns: [],
|
||||
rowCount: 0
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
rows: [],
|
||||
columns: [],
|
||||
rowCount: 1
|
||||
})
|
||||
|
||||
const { MaterialsToBeDeletedDAO } = await import(
|
||||
'../../../../src/main/services/database/materials-to-be-deleted-dao'
|
||||
)
|
||||
const dao = new MaterialsToBeDeletedDAO()
|
||||
|
||||
const result = await dao.upsertMaterial('M-001', 'tester')
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(queryMock).toHaveBeenCalledTimes(2)
|
||||
|
||||
const [updateSql, updateParams] = queryMock.mock.calls[0]
|
||||
const [insertSql, insertParams] = queryMock.mock.calls[1]
|
||||
|
||||
expect(updateSql).toContain('UPDATE "dbo"."MaterialsToBeDeleted"')
|
||||
expect(updateSql).toContain('WHERE MaterialCode = $2')
|
||||
expect(updateParams).toEqual(['tester', 'M-001'])
|
||||
|
||||
expect(insertSql).toContain('INSERT INTO "dbo"."MaterialsToBeDeleted" (MaterialCode, ManagerName)')
|
||||
expect(insertSql).toContain('WHERE NOT EXISTS')
|
||||
expect(insertSql).not.toContain('ON CONFLICT')
|
||||
expect(insertParams).toEqual(['M-001', 'tester'])
|
||||
})
|
||||
|
||||
it('reuses the PostgreSQL-safe path in updateManager', async () => {
|
||||
queryMock.mockResolvedValueOnce({
|
||||
rows: [],
|
||||
columns: [],
|
||||
rowCount: 1
|
||||
})
|
||||
|
||||
const { MaterialsToBeDeletedDAO } = await import(
|
||||
'../../../../src/main/services/database/materials-to-be-deleted-dao'
|
||||
)
|
||||
const dao = new MaterialsToBeDeletedDAO()
|
||||
|
||||
const result = await dao.updateManager('M-001', 'tester')
|
||||
|
||||
expect(result).toEqual({ success: true })
|
||||
expect(queryMock).toHaveBeenCalledTimes(1)
|
||||
expect((queryMock.mock.calls[0][0] as string)).toContain('UPDATE "dbo"."MaterialsToBeDeleted"')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,88 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const queryMock = vi.fn()
|
||||
const createMock = vi.fn()
|
||||
const trackDurationMock = vi.fn(async (fn: () => Promise<unknown>) => ({ result: await fn() }))
|
||||
|
||||
vi.mock('../../../../src/main/services/logger', () => ({
|
||||
createLogger: () => ({
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn()
|
||||
}),
|
||||
getRequestId: () => 'test-request-id',
|
||||
trackDuration: trackDurationMock
|
||||
}))
|
||||
|
||||
vi.mock('../../../../src/main/services/database/index', () => ({
|
||||
create: createMock
|
||||
}))
|
||||
|
||||
describe('MaterialsTypeToBeDeletedDAO (PostgreSQL compatibility)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
createMock.mockResolvedValue({
|
||||
type: 'postgresql',
|
||||
isConnected: () => true,
|
||||
query: queryMock,
|
||||
disconnect: vi.fn()
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to update-then-insert instead of ON CONFLICT for PostgreSQL inserts', async () => {
|
||||
queryMock
|
||||
.mockResolvedValueOnce({
|
||||
rows: [],
|
||||
columns: [],
|
||||
rowCount: 0
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
rows: [],
|
||||
columns: [],
|
||||
rowCount: 1
|
||||
})
|
||||
|
||||
const { MaterialsTypeToBeDeletedDAO } = await import(
|
||||
'../../../../src/main/services/database/materials-type-to-be-deleted-dao'
|
||||
)
|
||||
const dao = new MaterialsTypeToBeDeletedDAO()
|
||||
|
||||
const result = await dao.upsertMaterial('测试物料', 'tester')
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(queryMock).toHaveBeenCalledTimes(2)
|
||||
|
||||
const [updateSql, updateParams] = queryMock.mock.calls[0]
|
||||
const [insertSql, insertParams] = queryMock.mock.calls[1]
|
||||
|
||||
expect(updateSql).toContain('UPDATE "dbo"."MaterialsTypeToBeDeleted"')
|
||||
expect(updateSql).toContain('WHERE MaterialName = $2')
|
||||
expect(updateParams).toEqual(['tester', '测试物料'])
|
||||
|
||||
expect(insertSql).toContain('INSERT INTO "dbo"."MaterialsTypeToBeDeleted" (MaterialName, ManagerName)')
|
||||
expect(insertSql).toContain('WHERE NOT EXISTS')
|
||||
expect(insertSql).not.toContain('ON CONFLICT')
|
||||
expect(insertParams).toEqual(['测试物料', 'tester'])
|
||||
})
|
||||
|
||||
it('returns after the first update when the material already exists', async () => {
|
||||
queryMock.mockResolvedValueOnce({
|
||||
rows: [],
|
||||
columns: [],
|
||||
rowCount: 1
|
||||
})
|
||||
|
||||
const { MaterialsTypeToBeDeletedDAO } = await import(
|
||||
'../../../../src/main/services/database/materials-type-to-be-deleted-dao'
|
||||
)
|
||||
const dao = new MaterialsTypeToBeDeletedDAO()
|
||||
|
||||
const result = await dao.upsertMaterial('测试物料', 'tester')
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(queryMock).toHaveBeenCalledTimes(1)
|
||||
expect((queryMock.mock.calls[0][0] as string)).toContain('UPDATE "dbo"."MaterialsTypeToBeDeleted"')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user