- Remove unused imports (run, trackDuration, PerformanceTracker, ConfigManager, disconnectDb) flagged by ESLint - Remove unused isSlow variable in performance-monitor catch block - Add eslint-disable for require() in Playwright JS script - Fix logger-performance test flakiness by using vi.resetModules() with dynamic imports to prevent cached logger references across test files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
966 lines
28 KiB
TypeScript
966 lines
28 KiB
TypeScript
/**
|
|
* Data Access Object for DiscreteMaterialPlanData table
|
|
*
|
|
* Mirrors the Python DiscreteMaterialPlanDAO functionality:
|
|
* - Query operations for discrete material plan data
|
|
* - Support for querying by PlanNumber, SourceNumber
|
|
* - Deduplication by MaterialCode
|
|
* - Statistics gathering
|
|
*/
|
|
|
|
import { create, type IDatabaseService } from './index'
|
|
import { createDialect, type SqlDialect } from './dialects'
|
|
import { createLogger, getRequestId, trackDuration } from '../logger'
|
|
|
|
const log = createLogger('DiscreteMaterialPlanDAO')
|
|
|
|
/**
|
|
* Material plan record interface
|
|
*/
|
|
export interface MaterialPlanRecord {
|
|
id?: number
|
|
factory: string
|
|
materialStatus: string
|
|
planNumber: string
|
|
sourceNumber: string
|
|
materialType: string
|
|
productCode: string
|
|
productName: string
|
|
productUnit: string
|
|
productPlanQuantity: number
|
|
useDepartment: string
|
|
remark: string
|
|
creator: string
|
|
createDate: Date
|
|
approver: string
|
|
approveDate: Date
|
|
sequenceNumber: number
|
|
materialCode: string
|
|
materialName: string
|
|
specification: string
|
|
model: string
|
|
drawingNumber: string
|
|
materialQuality: string
|
|
planQuantity: number
|
|
unit: string
|
|
requiredDate: Date
|
|
warehouse: string
|
|
unitUsage: number
|
|
cumulativeOutputQuantity: number
|
|
bomVersion: string
|
|
}
|
|
|
|
/**
|
|
* Configuration for DiscreteMaterialPlanData table
|
|
*/
|
|
export const DISCRETE_MATERIAL_PLAN_CONFIG = {
|
|
COLUMNS: {
|
|
ID: 'ID',
|
|
FACTORY: 'Factory',
|
|
MATERIAL_STATUS: 'MaterialStatus',
|
|
PLAN_NUMBER: 'PlanNumber',
|
|
SOURCE_NUMBER: 'SourceNumber',
|
|
MATERIAL_TYPE: 'MaterialType',
|
|
PRODUCT_CODE: 'ProductCode',
|
|
PRODUCT_NAME: 'ProductName',
|
|
PRODUCT_UNIT: 'ProductUnit',
|
|
PRODUCT_PLAN_QUANTITY: 'ProductPlanQuantity',
|
|
USE_DEPARTMENT: 'UseDepartment',
|
|
REMARK: 'Remark',
|
|
CREATOR: 'Creator',
|
|
CREATE_DATE: 'CreateDate',
|
|
APPROVER: 'Approver',
|
|
APPROVE_DATE: 'ApproveDate',
|
|
SEQUENCE_NUMBER: 'SequenceNumber',
|
|
MATERIAL_CODE: 'MaterialCode',
|
|
MATERIAL_NAME: 'MaterialName',
|
|
SPECIFICATION: 'Specification',
|
|
MODEL: 'Model',
|
|
DRAWING_NUMBER: 'DrawingNumber',
|
|
MATERIAL_QUALITY: 'MaterialQuality',
|
|
PLAN_QUANTITY: 'PlanQuantity',
|
|
UNIT: 'Unit',
|
|
REQUIRED_DATE: 'RequiredDate',
|
|
WAREHOUSE: 'Warehouse',
|
|
UNIT_USAGE: 'UnitUsage',
|
|
CUMULATIVE_OUTPUT_QUANTITY: 'CumulativeOutputQuantity',
|
|
BOM_VERSION: 'BOMVersion'
|
|
}
|
|
} as const
|
|
|
|
/**
|
|
* DiscreteMaterialPlanDAO Class
|
|
*/
|
|
export class DiscreteMaterialPlanDAO {
|
|
private dbService: IDatabaseService | null = null
|
|
private dialect: SqlDialect | null = null
|
|
|
|
private getDialect(): SqlDialect {
|
|
if (!this.dialect) {
|
|
this.dialect = createDialect(this.dbService!.type)
|
|
}
|
|
return this.dialect
|
|
}
|
|
|
|
/**
|
|
* Get the appropriate table name based on database type
|
|
*/
|
|
private getTableName(): string {
|
|
return this.getDialect().quoteTableName('dbo', 'DiscreteMaterialPlanData')
|
|
}
|
|
|
|
/**
|
|
* Get database service instance using DatabaseFactory
|
|
*/
|
|
private async getDatabaseService(): Promise<IDatabaseService> {
|
|
if (this.dbService && this.dbService.isConnected()) {
|
|
return this.dbService
|
|
}
|
|
|
|
this.dbService = await create()
|
|
return this.dbService
|
|
}
|
|
|
|
// ==================== QUERY ALL ====================
|
|
|
|
/**
|
|
* Query all records from DiscreteMaterialPlanData table
|
|
* @returns List of all records
|
|
*/
|
|
async queryAll(): Promise<any[]> {
|
|
try {
|
|
const dbService = await this.getDatabaseService()
|
|
const tableName = this.getTableName()
|
|
|
|
const sqlString = `SELECT * FROM ${tableName}`
|
|
const result = await trackDuration(async () => await dbService.query(sqlString), {
|
|
operationName: 'DiscreteMaterialPlanDAO.queryAll',
|
|
context: { tableName, operationType: 'SELECT' }
|
|
})
|
|
|
|
return result.result.rows
|
|
} catch (error) {
|
|
log.error('Query all error', {
|
|
tableName: this.getTableName(),
|
|
operationType: 'SELECT',
|
|
requestId: getRequestId(),
|
|
error: error instanceof Error ? error.message : String(error)
|
|
})
|
|
return []
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Query all records with deduplication by MaterialCode
|
|
* Strategy: Keep first record for each MaterialCode
|
|
* Order: CreateDate ASC, SequenceNumber ASC
|
|
* @returns List of deduplicated records
|
|
*/
|
|
async queryAllDistinctByMaterialCode(): Promise<any[]> {
|
|
try {
|
|
const dbService = await this.getDatabaseService()
|
|
const tableName = this.getTableName()
|
|
|
|
const sqlString = `
|
|
WITH RankedRecords AS (
|
|
SELECT
|
|
*,
|
|
ROW_NUMBER() OVER (
|
|
PARTITION BY MaterialCode
|
|
ORDER BY CreateDate ASC, SequenceNumber ASC
|
|
) AS rn
|
|
FROM ${tableName}
|
|
WHERE MaterialCode IS NOT NULL
|
|
)
|
|
SELECT
|
|
Factory, MaterialStatus, PlanNumber, SourceNumber, MaterialType,
|
|
ProductCode, ProductName, ProductUnit, ProductPlanQuantity,
|
|
UseDepartment, Remark, Creator, CreateDate, Approver, ApproveDate,
|
|
SequenceNumber, MaterialCode, MaterialName, Specification, Model,
|
|
DrawingNumber, MaterialQuality, PlanQuantity, Unit, RequiredDate,
|
|
Warehouse, UnitUsage, CumulativeOutputQuantity, BOMVersion
|
|
FROM RankedRecords
|
|
WHERE rn = 1
|
|
`
|
|
|
|
const result = await trackDuration(async () => await dbService.query(sqlString), {
|
|
operationName: 'DiscreteMaterialPlanDAO.queryAllDistinctByMaterialCode',
|
|
context: { tableName: this.getTableName(), operationType: 'SELECT' }
|
|
})
|
|
return result.result.rows
|
|
} catch (error) {
|
|
log.error('Query all distinct by material code error', {
|
|
tableName: this.getTableName(),
|
|
operationType: 'SELECT',
|
|
requestId: getRequestId(),
|
|
error: error instanceof Error ? error.message : String(error)
|
|
})
|
|
return []
|
|
}
|
|
}
|
|
|
|
// ==================== QUERY BY SOURCE NUMBER ====================
|
|
|
|
/**
|
|
* Query records by SourceNumber list
|
|
* @param sourceNumbers - List of SourceNumber values
|
|
* @returns List of records
|
|
*/
|
|
async queryBySourceNumbers(sourceNumbers: string[]): Promise<any[]> {
|
|
if (!sourceNumbers || sourceNumbers.length === 0) {
|
|
return []
|
|
}
|
|
|
|
try {
|
|
const dbService = await this.getDatabaseService()
|
|
const tableName = this.getTableName()
|
|
const dialect = this.getDialect()
|
|
const batchSize = 1500
|
|
const allResults: any[] = []
|
|
|
|
for (let i = 0; i < sourceNumbers.length; i += batchSize) {
|
|
const batch = sourceNumbers.slice(i, i + batchSize)
|
|
const placeholders = dialect.params(batch.length)
|
|
|
|
const sqlString = `
|
|
SELECT *
|
|
FROM ${tableName}
|
|
WHERE SourceNumber IN (${placeholders})
|
|
`
|
|
|
|
const result = await trackDuration(async () => await dbService.query(sqlString, batch), {
|
|
operationName: 'DiscreteMaterialPlanDAO.queryBySourceNumbers',
|
|
context: {
|
|
tableName,
|
|
operationType: 'SELECT',
|
|
batchNumber: Math.floor(i / batchSize) + 1,
|
|
batchSize: batch.length
|
|
}
|
|
})
|
|
allResults.push(...result.result.rows)
|
|
}
|
|
|
|
return allResults
|
|
} catch (error) {
|
|
log.error('Query by source numbers error', {
|
|
tableName: this.getTableName(),
|
|
operationType: 'SELECT',
|
|
requestId: getRequestId(),
|
|
recordCount: sourceNumbers.length,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
})
|
|
return []
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Query records by SourceNumber with deduplication by MaterialCode
|
|
* Strategy: Keep first record for each MaterialCode
|
|
* Order: CreateDate ASC, SequenceNumber ASC
|
|
* @param sourceNumbers - List of SourceNumber values
|
|
* @returns List of deduplicated records
|
|
*/
|
|
async queryBySourceNumbersDistinct(sourceNumbers: string[]): Promise<any[]> {
|
|
if (!sourceNumbers || sourceNumbers.length === 0) {
|
|
return []
|
|
}
|
|
|
|
try {
|
|
const dbService = await this.getDatabaseService()
|
|
const tableName = this.getTableName()
|
|
const dialect = this.getDialect()
|
|
const batchSize = 1500
|
|
const allResults: any[] = []
|
|
|
|
for (let i = 0; i < sourceNumbers.length; i += batchSize) {
|
|
const batch = sourceNumbers.slice(i, i + batchSize)
|
|
const placeholders = dialect.params(batch.length)
|
|
|
|
const sqlString = `
|
|
WITH RankedRecords AS (
|
|
SELECT
|
|
*,
|
|
ROW_NUMBER() OVER (
|
|
PARTITION BY MaterialCode
|
|
ORDER BY CreateDate ASC, SequenceNumber ASC
|
|
) AS rn
|
|
FROM ${tableName}
|
|
WHERE SourceNumber IN (${placeholders})
|
|
AND MaterialCode IS NOT NULL
|
|
)
|
|
SELECT
|
|
Factory, MaterialStatus, PlanNumber, SourceNumber, MaterialType,
|
|
ProductCode, ProductName, ProductUnit, ProductPlanQuantity,
|
|
UseDepartment, Remark, Creator, CreateDate, Approver, ApproveDate,
|
|
SequenceNumber, MaterialCode, MaterialName, Specification, Model,
|
|
DrawingNumber, MaterialQuality, PlanQuantity, Unit, RequiredDate,
|
|
Warehouse, UnitUsage, CumulativeOutputQuantity, BOMVersion
|
|
FROM RankedRecords
|
|
WHERE rn = 1
|
|
`
|
|
|
|
const result = await trackDuration(async () => await dbService.query(sqlString, batch), {
|
|
operationName: 'DiscreteMaterialPlanDAO.queryBySourceNumbersDistinct',
|
|
context: {
|
|
tableName,
|
|
operationType: 'SELECT',
|
|
batchNumber: Math.floor(i / batchSize) + 1,
|
|
batchSize: batch.length
|
|
}
|
|
})
|
|
allResults.push(...result.result.rows)
|
|
}
|
|
|
|
return allResults
|
|
} catch (error) {
|
|
log.error('Query by source numbers distinct error', {
|
|
tableName: this.getTableName(),
|
|
operationType: 'SELECT',
|
|
requestId: getRequestId(),
|
|
recordCount: sourceNumbers.length,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
})
|
|
return []
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Query by production order number (SourceNumber)
|
|
* @param sourceNumber - SourceNumber value
|
|
* @returns List of records
|
|
*/
|
|
async queryBySourceNumber(sourceNumber: string): Promise<any[]> {
|
|
try {
|
|
const dbService = await this.getDatabaseService()
|
|
const tableName = this.getTableName()
|
|
const dialect = this.getDialect()
|
|
|
|
const placeholder = dialect.param(0)
|
|
const sqlString = `
|
|
SELECT *
|
|
FROM ${tableName}
|
|
WHERE SourceNumber = ${placeholder}
|
|
`
|
|
|
|
const result = await trackDuration(
|
|
async () => await dbService.query(sqlString, [sourceNumber]),
|
|
{
|
|
operationName: 'DiscreteMaterialPlanDAO.queryBySourceNumber',
|
|
context: { tableName, operationType: 'SELECT' }
|
|
}
|
|
)
|
|
return result.result.rows
|
|
} catch (error) {
|
|
log.error('Query by source number error', {
|
|
tableName: this.getTableName(),
|
|
operationType: 'SELECT',
|
|
requestId: getRequestId(),
|
|
error: error instanceof Error ? error.message : String(error)
|
|
})
|
|
return []
|
|
}
|
|
}
|
|
|
|
// ==================== QUERY BY PLAN NUMBER ====================
|
|
|
|
/**
|
|
* Query by plan number
|
|
* @param planNumber - PlanNumber value
|
|
* @returns List of records
|
|
*/
|
|
async queryByPlanNumber(planNumber: string): Promise<any[]> {
|
|
try {
|
|
const dbService = await this.getDatabaseService()
|
|
const tableName = this.getTableName()
|
|
const dialect = this.getDialect()
|
|
|
|
const placeholder = dialect.param(0)
|
|
const sqlString = `
|
|
SELECT *
|
|
FROM ${tableName}
|
|
WHERE PlanNumber = ${placeholder}
|
|
`
|
|
|
|
const result = await trackDuration(
|
|
async () => await dbService.query(sqlString, [planNumber]),
|
|
{
|
|
operationName: 'DiscreteMaterialPlanDAO.queryByPlanNumber',
|
|
context: { tableName, operationType: 'SELECT' }
|
|
}
|
|
)
|
|
return result.result.rows
|
|
} catch (error) {
|
|
log.error('Query by plan number error', {
|
|
tableName: this.getTableName(),
|
|
operationType: 'SELECT',
|
|
requestId: getRequestId(),
|
|
error: error instanceof Error ? error.message : String(error)
|
|
})
|
|
return []
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Query by multiple plan numbers
|
|
* @param planNumbers - List of PlanNumber values
|
|
* @returns List of records
|
|
*/
|
|
async queryByPlanNumbers(planNumbers: string[]): Promise<any[]> {
|
|
if (!planNumbers || planNumbers.length === 0) {
|
|
return []
|
|
}
|
|
|
|
try {
|
|
const dbService = await this.getDatabaseService()
|
|
const tableName = this.getTableName()
|
|
const dialect = this.getDialect()
|
|
const batchSize = 1500
|
|
const allResults: any[] = []
|
|
|
|
for (let i = 0; i < planNumbers.length; i += batchSize) {
|
|
const batch = planNumbers.slice(i, i + batchSize)
|
|
const placeholders = dialect.params(batch.length)
|
|
|
|
const sqlString = `
|
|
SELECT *
|
|
FROM ${tableName}
|
|
WHERE PlanNumber IN (${placeholders})
|
|
`
|
|
|
|
const result = await trackDuration(async () => await dbService.query(sqlString, batch), {
|
|
operationName: 'DiscreteMaterialPlanDAO.queryByPlanNumbers',
|
|
context: {
|
|
tableName,
|
|
operationType: 'SELECT',
|
|
batchNumber: Math.floor(i / batchSize) + 1,
|
|
batchSize: batch.length
|
|
}
|
|
})
|
|
allResults.push(...result.result.rows)
|
|
}
|
|
|
|
return allResults
|
|
} catch (error) {
|
|
log.error('Query by plan numbers error', {
|
|
tableName: this.getTableName(),
|
|
operationType: 'SELECT',
|
|
requestId: getRequestId(),
|
|
recordCount: planNumbers.length,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
})
|
|
return []
|
|
}
|
|
}
|
|
|
|
// ==================== DELETE OPERATIONS ====================
|
|
|
|
/**
|
|
* Delete records by SourceNumber list
|
|
* Uses batch processing for large lists
|
|
* @param sourceNumbers - List of SourceNumber values to delete
|
|
* @returns Number of records deleted
|
|
*/
|
|
async deleteBySourceNumbers(sourceNumbers: string[]): Promise<number> {
|
|
if (!sourceNumbers || sourceNumbers.length === 0) {
|
|
return 0
|
|
}
|
|
|
|
const batchId = getRequestId() || `delete-${Date.now()}`
|
|
let totalDeleted = 0
|
|
|
|
try {
|
|
const dbService = await this.getDatabaseService()
|
|
const tableName = this.getTableName()
|
|
const dialect = this.getDialect()
|
|
const batchSize = 2000
|
|
|
|
// Get unique source numbers
|
|
const uniqueSourceNumbers = [...new Set(sourceNumbers.filter(Boolean))]
|
|
const totalBatches = Math.ceil(uniqueSourceNumbers.length / batchSize)
|
|
|
|
log.info('Starting batch delete operation', {
|
|
tableName: this.getTableName(),
|
|
operationType: 'DELETE',
|
|
requestId: batchId,
|
|
totalRecords: uniqueSourceNumbers.length,
|
|
batchSize,
|
|
totalBatches
|
|
})
|
|
|
|
for (let i = 0; i < uniqueSourceNumbers.length; i += batchSize) {
|
|
const batch = uniqueSourceNumbers.slice(i, i + batchSize)
|
|
const batchNumber = Math.floor(i / batchSize) + 1
|
|
const placeholders = dialect.params(batch.length)
|
|
|
|
const sqlString = `
|
|
DELETE FROM ${tableName}
|
|
WHERE SourceNumber IN (${placeholders})
|
|
`
|
|
|
|
const result = await trackDuration(async () => await dbService.query(sqlString, batch), {
|
|
operationName: 'DiscreteMaterialPlanDAO.deleteBySourceNumbers',
|
|
context: {
|
|
tableName,
|
|
operationType: 'DELETE',
|
|
batchId,
|
|
batchNumber,
|
|
totalBatches,
|
|
batchSize: batch.length
|
|
}
|
|
})
|
|
const deletedCount = result.result.rowCount || 0
|
|
totalDeleted += deletedCount
|
|
|
|
log.debug('Deleted batch', {
|
|
batch: batchNumber,
|
|
totalBatches,
|
|
count: deletedCount,
|
|
batchId
|
|
})
|
|
}
|
|
|
|
log.info('Deleted records by source numbers', {
|
|
tableName: this.getTableName(),
|
|
operationType: 'DELETE',
|
|
requestId: batchId,
|
|
totalDeleted,
|
|
sourceNumberCount: uniqueSourceNumbers.length
|
|
})
|
|
|
|
return totalDeleted
|
|
} catch (error) {
|
|
log.error('Delete by source numbers error', {
|
|
tableName: this.getTableName(),
|
|
operationType: 'DELETE',
|
|
requestId: batchId,
|
|
totalDeleted,
|
|
recordCount: sourceNumbers.length,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
})
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// ==================== INSERT OPERATIONS ====================
|
|
|
|
/**
|
|
* Insert records in batches
|
|
* @param records - List of MaterialPlanRecord to insert
|
|
* @param batchSize - Number of records per batch (default: 1000, auto-adjusted for SQL Server)
|
|
* @returns Number of records inserted
|
|
*/
|
|
async batchInsert(records: MaterialPlanRecord[], batchSize = 1000): Promise<number> {
|
|
if (!records || records.length === 0) {
|
|
return 0
|
|
}
|
|
|
|
const batchId = getRequestId() || `insert-${Date.now()}`
|
|
let totalInserted = 0
|
|
|
|
try {
|
|
const dbService = await this.getDatabaseService()
|
|
const tableName = this.getTableName()
|
|
const dialect = this.getDialect()
|
|
|
|
// SQL Server has a limit of 2100 parameters per query
|
|
// Each record has 28 columns, so max rows per batch = 2100 / 28 = 75
|
|
// Leave some margin for query overhead
|
|
const columnsPerRow = 28
|
|
const effectiveBatchSize = Math.min(batchSize, dialect.maxBatchRows(columnsPerRow))
|
|
const totalBatches = Math.ceil(records.length / effectiveBatchSize)
|
|
|
|
log.info('Batch insert started', {
|
|
tableName,
|
|
operationType: 'INSERT',
|
|
requestId: batchId,
|
|
dbType: dbService.type,
|
|
columnsPerRow,
|
|
effectiveBatchSize,
|
|
totalRecords: records.length,
|
|
totalBatches
|
|
})
|
|
|
|
// Process in batches
|
|
for (let i = 0; i < records.length; i += effectiveBatchSize) {
|
|
const batch = records.slice(i, i + effectiveBatchSize)
|
|
const batchNumber = Math.floor(i / effectiveBatchSize) + 1
|
|
|
|
const inserted = await this.insertBatchWithTracking(
|
|
dbService,
|
|
tableName,
|
|
batch,
|
|
batchId,
|
|
batchNumber,
|
|
totalBatches
|
|
)
|
|
totalInserted += inserted
|
|
|
|
log.debug('Inserted batch', {
|
|
batch: batchNumber,
|
|
totalBatches,
|
|
count: inserted,
|
|
batchId
|
|
})
|
|
}
|
|
|
|
log.info('Batch insert completed', {
|
|
tableName,
|
|
operationType: 'INSERT',
|
|
requestId: batchId,
|
|
totalInserted,
|
|
batchSize: effectiveBatchSize,
|
|
totalBatches
|
|
})
|
|
|
|
return totalInserted
|
|
} catch (error) {
|
|
log.error('Batch insert error', {
|
|
tableName: this.getTableName(),
|
|
operationType: 'INSERT',
|
|
requestId: batchId,
|
|
totalInserted,
|
|
recordCount: records.length,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
})
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Insert a single batch of records with tracking
|
|
*/
|
|
private async insertBatchWithTracking(
|
|
dbService: IDatabaseService,
|
|
tableName: string,
|
|
records: MaterialPlanRecord[],
|
|
batchId: string,
|
|
batchNumber: number,
|
|
totalBatches: number
|
|
): Promise<number> {
|
|
if (records.length === 0) {
|
|
return 0
|
|
}
|
|
|
|
// Build column list (excluding id)
|
|
const columns = [
|
|
'Factory',
|
|
'MaterialStatus',
|
|
'PlanNumber',
|
|
'SourceNumber',
|
|
'MaterialType',
|
|
'ProductCode',
|
|
'ProductName',
|
|
'ProductUnit',
|
|
'ProductPlanQuantity',
|
|
'UseDepartment',
|
|
'Remark',
|
|
'Creator',
|
|
'CreateDate',
|
|
'Approver',
|
|
'ApproveDate',
|
|
'SequenceNumber',
|
|
'MaterialCode',
|
|
'MaterialName',
|
|
'Specification',
|
|
'Model',
|
|
'DrawingNumber',
|
|
'MaterialQuality',
|
|
'PlanQuantity',
|
|
'Unit',
|
|
'RequiredDate',
|
|
'Warehouse',
|
|
'UnitUsage',
|
|
'CumulativeOutputQuantity'
|
|
]
|
|
|
|
// Build parameterized insert
|
|
const values: any[] = []
|
|
const rowPlaceholders: string[] = []
|
|
|
|
records.forEach((record, rowIndex) => {
|
|
const rowValues = this.buildRowValues(record, columns, rowIndex, values)
|
|
rowPlaceholders.push(`(${rowValues.join(',')})`)
|
|
})
|
|
|
|
const sqlString = `
|
|
INSERT INTO ${tableName} (${columns.join(', ')})
|
|
VALUES ${rowPlaceholders.join(', ')}
|
|
`
|
|
|
|
const result = await trackDuration(async () => await dbService.query(sqlString, values), {
|
|
operationName: 'DiscreteMaterialPlanDAO.insertBatch',
|
|
context: {
|
|
tableName,
|
|
operationType: 'INSERT',
|
|
batchId,
|
|
batchNumber,
|
|
totalBatches,
|
|
recordCount: records.length
|
|
}
|
|
})
|
|
return result.result.rowCount || records.length
|
|
}
|
|
|
|
/**
|
|
* Insert a single batch of records (legacy method - kept for compatibility)
|
|
*/
|
|
private async insertBatch(
|
|
dbService: IDatabaseService,
|
|
tableName: string,
|
|
records: MaterialPlanRecord[]
|
|
): Promise<number> {
|
|
return this.insertBatchWithTracking(dbService, tableName, records, 'unknown', 1, 1)
|
|
}
|
|
|
|
/**
|
|
* Build parameter values for a single row
|
|
*/
|
|
private buildRowValues(
|
|
record: MaterialPlanRecord,
|
|
columns: string[],
|
|
_rowIndex: number,
|
|
values: any[]
|
|
): string[] {
|
|
const dialect = this.getDialect()
|
|
return columns.map((col) => {
|
|
const value = this.getColumnValue(record, col)
|
|
values.push(value)
|
|
|
|
return dialect.param(values.length - 1)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Get the value for a specific column from the record
|
|
*/
|
|
private getColumnValue(record: MaterialPlanRecord, column: string): any {
|
|
const columnMapping: Record<string, keyof MaterialPlanRecord> = {
|
|
Factory: 'factory',
|
|
MaterialStatus: 'materialStatus',
|
|
PlanNumber: 'planNumber',
|
|
SourceNumber: 'sourceNumber',
|
|
MaterialType: 'materialType',
|
|
ProductCode: 'productCode',
|
|
ProductName: 'productName',
|
|
ProductUnit: 'productUnit',
|
|
ProductPlanQuantity: 'productPlanQuantity',
|
|
UseDepartment: 'useDepartment',
|
|
Remark: 'remark',
|
|
Creator: 'creator',
|
|
CreateDate: 'createDate',
|
|
Approver: 'approver',
|
|
ApproveDate: 'approveDate',
|
|
SequenceNumber: 'sequenceNumber',
|
|
MaterialCode: 'materialCode',
|
|
MaterialName: 'materialName',
|
|
Specification: 'specification',
|
|
Model: 'model',
|
|
DrawingNumber: 'drawingNumber',
|
|
MaterialQuality: 'materialQuality',
|
|
PlanQuantity: 'planQuantity',
|
|
Unit: 'unit',
|
|
RequiredDate: 'requiredDate',
|
|
Warehouse: 'warehouse',
|
|
UnitUsage: 'unitUsage',
|
|
CumulativeOutputQuantity: 'cumulativeOutputQuantity'
|
|
}
|
|
|
|
const key = columnMapping[column]
|
|
if (!key) {
|
|
return null
|
|
}
|
|
|
|
const value = record[key]
|
|
|
|
// Handle null/undefined
|
|
if (value === null || value === undefined) {
|
|
return null
|
|
}
|
|
|
|
// Handle empty strings for string fields
|
|
if (typeof value === 'string' && value.trim() === '') {
|
|
return null
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
// ==================== UTILITY METHODS ====================
|
|
|
|
/**
|
|
* Count all records
|
|
* @returns Total number of records
|
|
*/
|
|
async countAll(): Promise<number> {
|
|
try {
|
|
const dbService = await this.getDatabaseService()
|
|
const tableName = this.getTableName()
|
|
|
|
const sqlString = `SELECT COUNT(*) as count FROM ${tableName}`
|
|
const result = await trackDuration(async () => await dbService.query(sqlString), {
|
|
operationName: 'DiscreteMaterialPlanDAO.countAll',
|
|
context: { tableName, operationType: 'SELECT' }
|
|
})
|
|
|
|
return result.result.rows.length > 0 ? (result.result.rows[0].count as number) : 0
|
|
} catch (error) {
|
|
log.error('Count all error', {
|
|
tableName: this.getTableName(),
|
|
operationType: 'SELECT',
|
|
requestId: getRequestId(),
|
|
error: error instanceof Error ? error.message : String(error)
|
|
})
|
|
return 0
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Count records by plan number
|
|
* @param planNumber - PlanNumber value
|
|
* @returns Number of records
|
|
*/
|
|
async countByPlanNumber(planNumber: string): Promise<number> {
|
|
try {
|
|
const dbService = await this.getDatabaseService()
|
|
const tableName = this.getTableName()
|
|
const dialect = this.getDialect()
|
|
|
|
const placeholder = dialect.param(0)
|
|
const sqlString = `
|
|
SELECT COUNT(*) as count
|
|
FROM ${tableName}
|
|
WHERE PlanNumber = ${placeholder}
|
|
`
|
|
|
|
const result = await trackDuration(
|
|
async () => await dbService.query(sqlString, [planNumber]),
|
|
{
|
|
operationName: 'DiscreteMaterialPlanDAO.countByPlanNumber',
|
|
context: { tableName, operationType: 'SELECT' }
|
|
}
|
|
)
|
|
return result.result.rows.length > 0 ? (result.result.rows[0].count as number) : 0
|
|
} catch (error) {
|
|
log.error('Count by plan number error', {
|
|
tableName: this.getTableName(),
|
|
operationType: 'SELECT',
|
|
requestId: getRequestId(),
|
|
error: error instanceof Error ? error.message : String(error)
|
|
})
|
|
return 0
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get unique material names
|
|
* @param sourceNumbers - Optional list of SourceNumber values to filter
|
|
* @returns List of unique material names
|
|
*/
|
|
async getUniqueMaterialNames(sourceNumbers?: string[]): Promise<string[]> {
|
|
try {
|
|
const dbService = await this.getDatabaseService()
|
|
const tableName = this.getTableName()
|
|
const dialect = this.getDialect()
|
|
|
|
if (sourceNumbers && sourceNumbers.length > 0) {
|
|
const batchSize = 1500
|
|
const allNames: string[] = []
|
|
|
|
for (let i = 0; i < sourceNumbers.length; i += batchSize) {
|
|
const batch = sourceNumbers.slice(i, i + batchSize)
|
|
const placeholders = dialect.params(batch.length)
|
|
|
|
const sqlString = `
|
|
SELECT DISTINCT MaterialName
|
|
FROM ${tableName}
|
|
WHERE SourceNumber IN (${placeholders})
|
|
AND MaterialName IS NOT NULL
|
|
`
|
|
|
|
const result = await trackDuration(async () => await dbService.query(sqlString, batch), {
|
|
operationName: 'DiscreteMaterialPlanDAO.getUniqueMaterialNames',
|
|
context: {
|
|
tableName,
|
|
operationType: 'SELECT',
|
|
batchNumber: Math.floor(i / batchSize) + 1,
|
|
batchSize: batch.length
|
|
}
|
|
})
|
|
allNames.push(
|
|
...result.result.rows.map((row) => row.MaterialName as string).filter(Boolean)
|
|
)
|
|
}
|
|
|
|
return allNames
|
|
} else {
|
|
const sqlString = `
|
|
SELECT DISTINCT MaterialName
|
|
FROM ${tableName}
|
|
WHERE MaterialName IS NOT NULL
|
|
`
|
|
|
|
const result = await trackDuration(async () => await dbService.query(sqlString), {
|
|
operationName: 'DiscreteMaterialPlanDAO.getUniqueMaterialNames',
|
|
context: { tableName, operationType: 'SELECT' }
|
|
})
|
|
return result.result.rows.map((row) => row.MaterialName as string).filter(Boolean)
|
|
}
|
|
} catch (error) {
|
|
log.error('Get unique material names error', {
|
|
tableName: this.getTableName(),
|
|
operationType: 'SELECT',
|
|
requestId: getRequestId(),
|
|
recordCount: sourceNumbers?.length || 0,
|
|
error: error instanceof Error ? error.message : String(error)
|
|
})
|
|
return []
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get statistics
|
|
* @returns Statistics object
|
|
*/
|
|
async getStatistics(): Promise<any> {
|
|
try {
|
|
const dbService = await this.getDatabaseService()
|
|
const tableName = this.getTableName()
|
|
|
|
const sqlString = `
|
|
SELECT
|
|
COUNT(*) as totalRecords,
|
|
COUNT(DISTINCT PlanNumber) as uniquePlans,
|
|
COUNT(DISTINCT SourceNumber) as uniqueOrders,
|
|
MIN(CreateDate) as earliestRecord,
|
|
MAX(CreateDate) as latestRecord
|
|
FROM ${tableName}
|
|
`
|
|
|
|
const result = await trackDuration(async () => await dbService.query(sqlString), {
|
|
operationName: 'DiscreteMaterialPlanDAO.getStatistics',
|
|
context: { tableName, operationType: 'SELECT' }
|
|
})
|
|
return result.result.rows.length > 0 ? result.result.rows[0] : {}
|
|
} catch (error) {
|
|
log.error('Get statistics error', {
|
|
tableName: this.getTableName(),
|
|
operationType: 'SELECT',
|
|
requestId: getRequestId(),
|
|
error: error instanceof Error ? error.message : String(error)
|
|
})
|
|
return {}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disconnect from database
|
|
*/
|
|
async disconnect(): Promise<void> {
|
|
if (this.dbService) {
|
|
await this.dbService.disconnect()
|
|
this.dbService = null
|
|
this.dialect = null
|
|
}
|
|
}
|
|
}
|