Compare commits
3 Commits
e2669af870
...
4a7c220baa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a7c220baa | ||
|
|
f51cae0f6f | ||
|
|
7601b5f176 |
@@ -1,11 +1,11 @@
|
|||||||
import type { WebContents } from 'electron'
|
import type { WebContents } from 'electron'
|
||||||
import type { MySqlService } from '../database/mysql'
|
import type { IDatabaseService } from '../../types/database.types'
|
||||||
import type { SqlServerService } from '../database/sql-server'
|
|
||||||
import { ErpAuthService } from '../erp/erp-auth'
|
import { ErpAuthService } from '../erp/erp-auth'
|
||||||
import { CleanerService } from '../erp/cleaner'
|
import { CleanerService } from '../erp/cleaner'
|
||||||
import { OrderNumberResolver } from '../erp/order-resolver'
|
import { OrderNumberResolver } from '../erp/order-resolver'
|
||||||
import { MySqlService as MySqlServiceImpl } from '../database/mysql'
|
import { MySqlService as MySqlServiceImpl } from '../database/mysql'
|
||||||
import { SqlServerService as SqlServerServiceImpl } from '../database/sql-server'
|
import { SqlServerService as SqlServerServiceImpl } from '../database/sql-server'
|
||||||
|
import { PostgreSqlService as PostgreSqlServiceImpl } from '../database/postgresql'
|
||||||
import { ConfigManager } from '../config/config-manager'
|
import { ConfigManager } from '../config/config-manager'
|
||||||
import { ResultExporter } from '../excel/result-exporter'
|
import { ResultExporter } from '../excel/result-exporter'
|
||||||
import { CleanerReportGenerator } from '../report/cleaner-report-generator'
|
import { CleanerReportGenerator } from '../report/cleaner-report-generator'
|
||||||
@@ -26,13 +26,11 @@ import type {
|
|||||||
|
|
||||||
const log = createLogger('CleanerApplicationService')
|
const log = createLogger('CleanerApplicationService')
|
||||||
|
|
||||||
type DatabaseService = MySqlService | SqlServerService
|
|
||||||
|
|
||||||
export class CleanerApplicationService {
|
export class CleanerApplicationService {
|
||||||
async runCleaner(eventSender: WebContents, input: CleanerInput): Promise<CleanerResult> {
|
async runCleaner(eventSender: WebContents, input: CleanerInput): Promise<CleanerResult> {
|
||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
let authService: ErpAuthService | null = null
|
let authService: ErpAuthService | null = null
|
||||||
let dbService: DatabaseService | null = null
|
let dbService: IDatabaseService | null = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log.info('Fetching ERP configuration from database...')
|
log.info('Fetching ERP configuration from database...')
|
||||||
@@ -46,7 +44,7 @@ export class CleanerApplicationService {
|
|||||||
const configManager = ConfigManager.getInstance()
|
const configManager = ConfigManager.getInstance()
|
||||||
const dbType = configManager.getDatabaseType()
|
const dbType = configManager.getDatabaseType()
|
||||||
log.info(
|
log.info(
|
||||||
`Connecting to ${dbType === 'sqlserver' ? 'SQL Server' : 'MySQL'} for order resolution...`
|
`Connecting to ${dbType === 'sqlserver' ? 'SQL Server' : dbType === 'postgresql' ? 'PostgreSQL' : 'MySQL'} for order resolution...`
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -201,7 +199,7 @@ export class CleanerApplicationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getDatabaseService(): Promise<DatabaseService> {
|
private async getDatabaseService(): Promise<IDatabaseService> {
|
||||||
const configManager = ConfigManager.getInstance()
|
const configManager = ConfigManager.getInstance()
|
||||||
const config = configManager.getConfig()
|
const config = configManager.getConfig()
|
||||||
const dbType = configManager.getDatabaseType()
|
const dbType = configManager.getDatabaseType()
|
||||||
@@ -223,6 +221,19 @@ export class CleanerApplicationService {
|
|||||||
return sqlServerService
|
return sqlServerService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dbType === 'postgresql') {
|
||||||
|
const dbConfig = config.database.postgresql
|
||||||
|
const pgService = new PostgreSqlServiceImpl({
|
||||||
|
host: dbConfig.host,
|
||||||
|
port: dbConfig.port,
|
||||||
|
user: dbConfig.username,
|
||||||
|
password: dbConfig.password,
|
||||||
|
database: dbConfig.database
|
||||||
|
})
|
||||||
|
await pgService.connect()
|
||||||
|
return pgService
|
||||||
|
}
|
||||||
|
|
||||||
const dbConfig = config.database.mysql
|
const dbConfig = config.database.mysql
|
||||||
const mysqlService = new MySqlServiceImpl({
|
const mysqlService = new MySqlServiceImpl({
|
||||||
host: dbConfig.host,
|
host: dbConfig.host,
|
||||||
|
|||||||
@@ -15,9 +15,20 @@ export type { PostgreSqlConfig } from '../../types/database.types'
|
|||||||
* SQL keywords that should NOT be double-quoted during identifier preprocessing.
|
* SQL keywords that should NOT be double-quoted during identifier preprocessing.
|
||||||
* PostgreSQL lowercases unquoted identifiers, but SSMA-migrated databases
|
* PostgreSQL lowercases unquoted identifiers, but SSMA-migrated databases
|
||||||
* have uppercase column names that require double-quoting to preserve case.
|
* have uppercase column names that require double-quoting to preserve case.
|
||||||
|
*
|
||||||
|
* This list covers PostgreSQL reserved words across multiple categories:
|
||||||
|
* - DML (Data Manipulation Language)
|
||||||
|
* - DDL (Data Definition Language)
|
||||||
|
* - Window functions
|
||||||
|
* - CTEs (Common Table Expressions)
|
||||||
|
* - Advanced GROUP BY clauses
|
||||||
|
* - JSON operations
|
||||||
|
* - Type system
|
||||||
|
* - Table sampling
|
||||||
|
* - Transaction control
|
||||||
*/
|
*/
|
||||||
const SQL_KEYWORDS = new Set([
|
const SQL_KEYWORDS = new Set([
|
||||||
// DML
|
// ==================== DML (Data Manipulation Language) ====================
|
||||||
'SELECT',
|
'SELECT',
|
||||||
'FROM',
|
'FROM',
|
||||||
'WHERE',
|
'WHERE',
|
||||||
@@ -33,7 +44,8 @@ const SQL_KEYWORDS = new Set([
|
|||||||
'UPDATE',
|
'UPDATE',
|
||||||
'SET',
|
'SET',
|
||||||
'DELETE',
|
'DELETE',
|
||||||
// Ordering & limiting
|
|
||||||
|
// ==================== Ordering & Limiting ====================
|
||||||
'ORDER',
|
'ORDER',
|
||||||
'BY',
|
'BY',
|
||||||
'ASC',
|
'ASC',
|
||||||
@@ -44,7 +56,8 @@ const SQL_KEYWORDS = new Set([
|
|||||||
'NEXT',
|
'NEXT',
|
||||||
'ROWS',
|
'ROWS',
|
||||||
'ONLY',
|
'ONLY',
|
||||||
// Joins
|
|
||||||
|
// ==================== Joins ====================
|
||||||
'JOIN',
|
'JOIN',
|
||||||
'LEFT',
|
'LEFT',
|
||||||
'RIGHT',
|
'RIGHT',
|
||||||
@@ -53,16 +66,67 @@ const SQL_KEYWORDS = new Set([
|
|||||||
'CROSS',
|
'CROSS',
|
||||||
'FULL',
|
'FULL',
|
||||||
'ON',
|
'ON',
|
||||||
// Set operations
|
'NATURAL',
|
||||||
|
'LATERAL',
|
||||||
|
|
||||||
|
// ==================== Set Operations ====================
|
||||||
'UNION',
|
'UNION',
|
||||||
'ALL',
|
'ALL',
|
||||||
'INTERSECT',
|
'INTERSECT',
|
||||||
'EXCEPT',
|
'EXCEPT',
|
||||||
// Grouping
|
|
||||||
|
// ==================== Grouping & Aggregation ====================
|
||||||
'GROUP',
|
'GROUP',
|
||||||
'HAVING',
|
'HAVING',
|
||||||
'DISTINCT',
|
'DISTINCT',
|
||||||
// DDL
|
'GROUPING',
|
||||||
|
'SETS',
|
||||||
|
'ROLLUP',
|
||||||
|
'CUBE',
|
||||||
|
'FILTER',
|
||||||
|
'WITHIN',
|
||||||
|
|
||||||
|
// ==================== Window Functions ====================
|
||||||
|
'OVER',
|
||||||
|
'PARTITION',
|
||||||
|
'WINDOW',
|
||||||
|
'RANGE',
|
||||||
|
'UNBOUNDED',
|
||||||
|
'PRECEDING',
|
||||||
|
'FOLLOWING',
|
||||||
|
'CURRENT',
|
||||||
|
'ROW',
|
||||||
|
'GROUPS',
|
||||||
|
'EXCLUDE',
|
||||||
|
'TIES',
|
||||||
|
'RANK',
|
||||||
|
'DENSE_RANK',
|
||||||
|
'ROW_NUMBER',
|
||||||
|
'NTILE',
|
||||||
|
'LAG',
|
||||||
|
'LEAD',
|
||||||
|
'FIRST_VALUE',
|
||||||
|
'LAST_VALUE',
|
||||||
|
'NTH_VALUE',
|
||||||
|
|
||||||
|
// ==================== CTE (Common Table Expressions) ====================
|
||||||
|
'WITH',
|
||||||
|
'RECURSIVE',
|
||||||
|
'MATERIALIZED',
|
||||||
|
'SEARCH',
|
||||||
|
'CYCLE',
|
||||||
|
'PATH',
|
||||||
|
'ROOT',
|
||||||
|
'SIBLINGS',
|
||||||
|
|
||||||
|
// ==================== CASE Expressions ====================
|
||||||
|
'CASE',
|
||||||
|
'WHEN',
|
||||||
|
'THEN',
|
||||||
|
'ELSE',
|
||||||
|
'END',
|
||||||
|
|
||||||
|
// ==================== DDL (Data Definition Language) ====================
|
||||||
'CREATE',
|
'CREATE',
|
||||||
'ALTER',
|
'ALTER',
|
||||||
'DROP',
|
'DROP',
|
||||||
@@ -73,7 +137,15 @@ const SQL_KEYWORDS = new Set([
|
|||||||
'MODIFY',
|
'MODIFY',
|
||||||
'RENAME',
|
'RENAME',
|
||||||
'TO',
|
'TO',
|
||||||
// PostgreSQL specific
|
'GENERATED',
|
||||||
|
'ALWAYS',
|
||||||
|
'IDENTITY',
|
||||||
|
'INCLUDE',
|
||||||
|
'TEMP',
|
||||||
|
'TEMPORARY',
|
||||||
|
'UNLOGGED',
|
||||||
|
|
||||||
|
// ==================== PostgreSQL Specific - UPSERT/MERGE ====================
|
||||||
'CONFLICT',
|
'CONFLICT',
|
||||||
'DO',
|
'DO',
|
||||||
'NOTHING',
|
'NOTHING',
|
||||||
@@ -82,32 +154,76 @@ const SQL_KEYWORDS = new Set([
|
|||||||
'MERGE',
|
'MERGE',
|
||||||
'USING',
|
'USING',
|
||||||
'MATCHED',
|
'MATCHED',
|
||||||
'WHEN',
|
|
||||||
'THEN',
|
|
||||||
'ELSE',
|
|
||||||
'END',
|
|
||||||
'TARGET',
|
'TARGET',
|
||||||
'SOURCE',
|
'SOURCE',
|
||||||
// Functions
|
|
||||||
|
// ==================== Aggregate Functions ====================
|
||||||
'COUNT',
|
'COUNT',
|
||||||
'SUM',
|
'SUM',
|
||||||
'AVG',
|
'AVG',
|
||||||
'MIN',
|
'MIN',
|
||||||
'MAX',
|
'MAX',
|
||||||
'EXISTS',
|
'EXISTS',
|
||||||
'CURRENT_TIMESTAMP',
|
|
||||||
'NOW',
|
|
||||||
'GETDATE',
|
|
||||||
'COALESCE',
|
'COALESCE',
|
||||||
'NULLIF',
|
'NULLIF',
|
||||||
'CAST',
|
'CAST',
|
||||||
'AS',
|
'AS',
|
||||||
// Transaction
|
|
||||||
|
// ==================== JSON Operations ====================
|
||||||
|
'JSON',
|
||||||
|
'JSONB',
|
||||||
|
'JSON_ARRAY',
|
||||||
|
'JSON_OBJECT',
|
||||||
|
'JSON_AGG',
|
||||||
|
'JSONB_AGG',
|
||||||
|
'JSONB_OBJECT_AGG',
|
||||||
|
|
||||||
|
// ==================== Types & Casting ====================
|
||||||
|
'DECIMAL',
|
||||||
|
'NUMERIC',
|
||||||
|
'BOOLEAN',
|
||||||
|
'CHARACTER',
|
||||||
|
'VARYING',
|
||||||
|
'PRECISION',
|
||||||
|
'REAL',
|
||||||
|
'DOUBLE',
|
||||||
|
'FLOAT',
|
||||||
|
'TEXT',
|
||||||
|
'INTEGER',
|
||||||
|
'SERIAL',
|
||||||
|
'BIGINT',
|
||||||
|
'SMALLINT',
|
||||||
|
'DATE',
|
||||||
|
'TIME',
|
||||||
|
'TIMESTAMP',
|
||||||
|
'TIMESTAMPTZ',
|
||||||
|
'TIMEZONE',
|
||||||
|
'INTERVAL',
|
||||||
|
'BIGSERIAL',
|
||||||
|
'SMALLSERIAL',
|
||||||
|
|
||||||
|
// ==================== Table Sampling ====================
|
||||||
|
'TABLESAMPLE',
|
||||||
|
'BERNOULLI',
|
||||||
|
'SYSTEM',
|
||||||
|
'REPEATABLE',
|
||||||
|
'SEED',
|
||||||
|
|
||||||
|
// ==================== Transaction Control ====================
|
||||||
'BEGIN',
|
'BEGIN',
|
||||||
'COMMIT',
|
'COMMIT',
|
||||||
'ROLLBACK',
|
'ROLLBACK',
|
||||||
'SAVEPOINT',
|
'SAVEPOINT',
|
||||||
// Types & values
|
'WORK',
|
||||||
|
'ISOLATION',
|
||||||
|
'LEVEL',
|
||||||
|
'READ',
|
||||||
|
'WRITE',
|
||||||
|
'COMMITTED',
|
||||||
|
'REPEATABLE',
|
||||||
|
'SERIALIZABLE',
|
||||||
|
|
||||||
|
// ==================== Types & Values ====================
|
||||||
'TRUE',
|
'TRUE',
|
||||||
'FALSE',
|
'FALSE',
|
||||||
'DEFAULT',
|
'DEFAULT',
|
||||||
@@ -118,23 +234,112 @@ const SQL_KEYWORDS = new Set([
|
|||||||
'CONSTRAINT',
|
'CONSTRAINT',
|
||||||
'UNIQUE',
|
'UNIQUE',
|
||||||
'CHECK',
|
'CHECK',
|
||||||
'CASE',
|
'NULLS',
|
||||||
|
'FIRST',
|
||||||
|
'LAST',
|
||||||
|
|
||||||
|
// ==================== Scalar & String Functions ====================
|
||||||
|
'UPPER',
|
||||||
|
'LOWER',
|
||||||
|
'TRIM',
|
||||||
|
'LTRIM',
|
||||||
|
'RTRIM',
|
||||||
|
'BTRIM',
|
||||||
|
'SUBSTRING',
|
||||||
|
'CONCAT',
|
||||||
|
'LENGTH',
|
||||||
|
'CHAR_LENGTH',
|
||||||
|
'CHARACTER_LENGTH',
|
||||||
|
'REPLACE',
|
||||||
|
'POSITION',
|
||||||
|
'OVERLAY',
|
||||||
|
'LPAD',
|
||||||
|
'RPAD',
|
||||||
|
'REPEAT',
|
||||||
|
'REVERSE',
|
||||||
|
'SPLIT_PART',
|
||||||
|
'INITCAP',
|
||||||
|
'NORMALIZE',
|
||||||
|
'CHR',
|
||||||
|
'ASCII',
|
||||||
|
'FORMAT',
|
||||||
|
|
||||||
|
// ==================== Numeric Functions ====================
|
||||||
|
'ABS',
|
||||||
|
'CEIL',
|
||||||
|
'CEILING',
|
||||||
|
'FLOOR',
|
||||||
|
'ROUND',
|
||||||
|
'POWER',
|
||||||
|
'SQRT',
|
||||||
|
'MOD',
|
||||||
|
'SIGN',
|
||||||
|
'TRUNC',
|
||||||
|
|
||||||
|
// ==================== Date/Time Functions ====================
|
||||||
|
'EXTRACT',
|
||||||
|
'DATE_TRUNC',
|
||||||
|
'TO_CHAR',
|
||||||
|
'TO_DATE',
|
||||||
|
'TO_TIMESTAMP',
|
||||||
|
'TO_NUMBER',
|
||||||
|
'AGE',
|
||||||
|
|
||||||
|
// ==================== Pattern Matching ====================
|
||||||
'BETWEEN',
|
'BETWEEN',
|
||||||
'LIKE',
|
'LIKE',
|
||||||
'ILIKE',
|
'ILIKE',
|
||||||
|
'SIMILAR',
|
||||||
|
'ESCAPE',
|
||||||
'ANY',
|
'ANY',
|
||||||
'SOME',
|
'SOME',
|
||||||
// Common
|
|
||||||
'IF',
|
// ==================== Functions & Procedures ====================
|
||||||
'WITH',
|
|
||||||
'RECURSIVE',
|
|
||||||
'OVER',
|
|
||||||
'PARTITION',
|
|
||||||
'WINDOW',
|
|
||||||
'ROW',
|
|
||||||
'FIRST',
|
|
||||||
'AFTER',
|
'AFTER',
|
||||||
'BEFORE'
|
'BEFORE',
|
||||||
|
'EACH',
|
||||||
|
'STATEMENT',
|
||||||
|
'TRIGGER',
|
||||||
|
'FUNCTION',
|
||||||
|
'PROCEDURE',
|
||||||
|
'LANGUAGE',
|
||||||
|
'SQL',
|
||||||
|
'PLPGSQL',
|
||||||
|
'RETURNS',
|
||||||
|
'CALLED',
|
||||||
|
'STRICT',
|
||||||
|
'SECURITY',
|
||||||
|
'INVOKER',
|
||||||
|
'DEFINER',
|
||||||
|
'VOLATILE',
|
||||||
|
'STABLE',
|
||||||
|
'IMMUTABLE',
|
||||||
|
'PARALLEL',
|
||||||
|
'SAFE',
|
||||||
|
'RESTRICTED',
|
||||||
|
'UNSAFE',
|
||||||
|
|
||||||
|
// ==================== Utility Commands ====================
|
||||||
|
'CONCURRENTLY',
|
||||||
|
'REINDEX',
|
||||||
|
'VACUUM',
|
||||||
|
'ANALYZE',
|
||||||
|
'EXPLAIN',
|
||||||
|
'LOCAL',
|
||||||
|
'GLOBAL',
|
||||||
|
'ORDINALITY',
|
||||||
|
'FREEZE',
|
||||||
|
'VERBOSE',
|
||||||
|
'BUFFERS',
|
||||||
|
'FORMAT',
|
||||||
|
'XML',
|
||||||
|
'YAML',
|
||||||
|
|
||||||
|
// ==================== Additional Reserved Words ====================
|
||||||
|
'IF',
|
||||||
|
'CURRENT_TIMESTAMP',
|
||||||
|
'NOW',
|
||||||
|
'GETDATE'
|
||||||
])
|
])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -286,7 +491,31 @@ export class PostgreSqlService implements IDatabaseService {
|
|||||||
user: this.config.user,
|
user: this.config.user,
|
||||||
password: this.config.password,
|
password: this.config.password,
|
||||||
database: this.config.database,
|
database: this.config.database,
|
||||||
max: this.config.maxPoolSize ?? 10
|
max: this.config.maxPoolSize ?? 10,
|
||||||
|
/**
|
||||||
|
* Connection timeout in milliseconds.
|
||||||
|
* Time to wait when connecting to PostgreSQL before failing.
|
||||||
|
* Prevents hanging during network issues or server overload.
|
||||||
|
*/
|
||||||
|
connectionTimeoutMillis: 10000,
|
||||||
|
/**
|
||||||
|
* PostgreSQL statement timeout in milliseconds.
|
||||||
|
* Limits execution time for individual SQL statements.
|
||||||
|
* Prevents long-running queries from blocking the connection pool.
|
||||||
|
*/
|
||||||
|
statement_timeout: 30000,
|
||||||
|
/**
|
||||||
|
* Idle connection timeout in milliseconds.
|
||||||
|
* Closes connections that have been idle for this duration.
|
||||||
|
* Frees up pool resources and prevents stale connections.
|
||||||
|
*/
|
||||||
|
idleTimeoutMillis: 30000,
|
||||||
|
/**
|
||||||
|
* Query timeout in milliseconds (pg driver level).
|
||||||
|
* Fallback protection to abort queries that exceed this duration.
|
||||||
|
* Should be longer than statement_timeout to allow PG to handle first.
|
||||||
|
*/
|
||||||
|
query_timeout: 60000
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test connection
|
// Test connection
|
||||||
|
|||||||
@@ -58,23 +58,34 @@ export class OrderNumberResolver {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get table name based on database type
|
* Get table name based on database type
|
||||||
* Converts MySQL schema_tablename format to SQL Server [schema].[tablename] format
|
* Converts schema_tablename format to database-specific quoting:
|
||||||
* e.g., productionContractData_26年压力表合同数据 -> [productionContractData].[26年压力表合同数据]
|
* - SQL Server: [schema].[tablename]
|
||||||
* dbo_MaterialsToBeDeleted -> [dbo].[MaterialsToBeDeleted]
|
* - PostgreSQL: "schema"."tablename"
|
||||||
|
* - MySQL: schema_tablename (as-is)
|
||||||
|
* e.g., productionContractData_26年压力表合同数据 ->
|
||||||
|
* SQL Server: [productionContractData].[26年压力表合同数据]
|
||||||
|
* PostgreSQL: "productionContractData"."26年压力表合同数据"
|
||||||
|
* MySQL: productionContractData_26年压力表合同数据
|
||||||
*/
|
*/
|
||||||
private getTableName(tableName: string): string {
|
private getTableName(tableName: string): string {
|
||||||
if (this.dbService.type === 'sqlserver') {
|
if (this.dbService.type === 'sqlserver' || this.dbService.type === 'postgresql') {
|
||||||
// Find the FIRST underscore to split schema and table name
|
// Find the FIRST underscore to split schema and table name
|
||||||
// This handles patterns like: schema_tablename
|
// This handles patterns like: schema_tablename
|
||||||
const firstUnderscoreIndex = tableName.indexOf('_')
|
const firstUnderscoreIndex = tableName.indexOf('_')
|
||||||
if (firstUnderscoreIndex > 0) {
|
if (firstUnderscoreIndex > 0) {
|
||||||
const schema = tableName.substring(0, firstUnderscoreIndex)
|
const schema = tableName.substring(0, firstUnderscoreIndex)
|
||||||
const actualTableName = tableName.substring(firstUnderscoreIndex + 1)
|
const actualTableName = tableName.substring(firstUnderscoreIndex + 1)
|
||||||
|
if (this.dbService.type === 'sqlserver') {
|
||||||
return `[${schema}].[${actualTableName}]`
|
return `[${schema}].[${actualTableName}]`
|
||||||
}
|
}
|
||||||
// If no underscore found, default to dbo schema
|
return `"${schema}"."${actualTableName}"`
|
||||||
|
}
|
||||||
|
// If no underscore found, default schema
|
||||||
|
if (this.dbService.type === 'sqlserver') {
|
||||||
return `[dbo].[${tableName}]`
|
return `[dbo].[${tableName}]`
|
||||||
}
|
}
|
||||||
|
return `"public"."${tableName}"`
|
||||||
|
}
|
||||||
return tableName
|
return tableName
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +118,12 @@ export class OrderNumberResolver {
|
|||||||
// 使用 COLLATE 指定不区分大小写的排序规则
|
// 使用 COLLATE 指定不区分大小写的排序规则
|
||||||
sql = `SELECT TOP 1 [${dbConfig.FIELD_ORDER_NUMBER}] FROM ${tableName} WHERE [${dbConfig.FIELD_PRODUCTION_ID}] COLLATE SQL_Latin1_General_CP1_CI_AS = @p0`
|
sql = `SELECT TOP 1 [${dbConfig.FIELD_ORDER_NUMBER}] FROM ${tableName} WHERE [${dbConfig.FIELD_PRODUCTION_ID}] COLLATE SQL_Latin1_General_CP1_CI_AS = @p0`
|
||||||
params = [productionId]
|
params = [productionId]
|
||||||
|
} else if (this.dbService.type === 'postgresql') {
|
||||||
|
// PostgreSQL: 使用双引号保护中文标识符,UPPER 实现不区分大小写
|
||||||
|
// prepareSql() 会保留已双引号包裹的标识符
|
||||||
|
// 注意:getTableName() 已返回带双引号的表名,不应再加引号
|
||||||
|
sql = `SELECT DISTINCT "${dbConfig.FIELD_PRODUCTION_ID}", "${dbConfig.FIELD_ORDER_NUMBER}" FROM ${tableName} WHERE UPPER("${dbConfig.FIELD_PRODUCTION_ID}") = UPPER($1) LIMIT 1`
|
||||||
|
params = [productionId]
|
||||||
} else {
|
} else {
|
||||||
// MySQL 默认不区分大小写,但显式使用 UPPER 确保一致性
|
// MySQL 默认不区分大小写,但显式使用 UPPER 确保一致性
|
||||||
sql = `SELECT \`${dbConfig.FIELD_ORDER_NUMBER}\` FROM \`${tableName}\` WHERE UPPER(\`${dbConfig.FIELD_PRODUCTION_ID}\`) = UPPER(?) LIMIT 1`
|
sql = `SELECT \`${dbConfig.FIELD_ORDER_NUMBER}\` FROM \`${tableName}\` WHERE UPPER(\`${dbConfig.FIELD_PRODUCTION_ID}\`) = UPPER(?) LIMIT 1`
|
||||||
@@ -155,6 +172,11 @@ export class OrderNumberResolver {
|
|||||||
// P0: Use DISTINCT to prevent duplicates from one-to-many relationships
|
// P0: Use DISTINCT to prevent duplicates from one-to-many relationships
|
||||||
// 使用 COLLATE 指定不区分大小写的排序规则
|
// 使用 COLLATE 指定不区分大小写的排序规则
|
||||||
sql = `SELECT DISTINCT [${dbConfig.FIELD_PRODUCTION_ID}], [${dbConfig.FIELD_ORDER_NUMBER}] FROM ${tableName} WHERE [${dbConfig.FIELD_PRODUCTION_ID}] COLLATE SQL_Latin1_General_CP1_CI_AS IN (${placeholders})`
|
sql = `SELECT DISTINCT [${dbConfig.FIELD_PRODUCTION_ID}], [${dbConfig.FIELD_ORDER_NUMBER}] FROM ${tableName} WHERE [${dbConfig.FIELD_PRODUCTION_ID}] COLLATE SQL_Latin1_General_CP1_CI_AS IN (${placeholders})`
|
||||||
|
} else if (this.dbService.type === 'postgresql') {
|
||||||
|
// PostgreSQL: 使用双引号保护中文标识符,UPPER 实现不区分大小写
|
||||||
|
// 注意:getTableName() 已返回带双引号的表名,不应再加引号
|
||||||
|
const pgPlaceholders = uniqueProductionIds.map((_, i) => `UPPER($${i + 1})`).join(', ')
|
||||||
|
sql = `SELECT DISTINCT "${dbConfig.FIELD_PRODUCTION_ID}", "${dbConfig.FIELD_ORDER_NUMBER}" FROM ${tableName} WHERE UPPER("${dbConfig.FIELD_PRODUCTION_ID}") IN (${pgPlaceholders})`
|
||||||
} else {
|
} else {
|
||||||
const idPlaceholders = uniqueProductionIds.map(() => 'UPPER(?)').join(', ')
|
const idPlaceholders = uniqueProductionIds.map(() => 'UPPER(?)').join(', ')
|
||||||
// P0: Use DISTINCT to prevent duplicates from one-to-many relationships
|
// P0: Use DISTINCT to prevent duplicates from one-to-many relationships
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export async function getSourceNumbersFromInputs(
|
|||||||
const productionIds: string[] = []
|
const productionIds: string[] = []
|
||||||
const orderNumbers: string[] = []
|
const orderNumbers: string[] = []
|
||||||
const configManager = ConfigManager.getInstance()
|
const configManager = ConfigManager.getInstance()
|
||||||
const isSqlServer = configManager.getDatabaseType() === 'sqlserver'
|
const dbType = configManager.getDatabaseType()
|
||||||
|
|
||||||
for (const item of inputs) {
|
for (const item of inputs) {
|
||||||
const type = identifyInputType(item)
|
const type = identifyInputType(item)
|
||||||
@@ -44,7 +44,7 @@ export async function getSourceNumbersFromInputs(
|
|||||||
const contractTableName = getValidationTableName('productionContractData_26年压力表合同数据')
|
const contractTableName = getValidationTableName('productionContractData_26年压力表合同数据')
|
||||||
const batchSize = 2000
|
const batchSize = 2000
|
||||||
|
|
||||||
if (isSqlServer) {
|
if (dbType === 'sqlserver') {
|
||||||
const sql = await import('mssql')
|
const sql = await import('mssql')
|
||||||
const allOrderNumbers: string[] = []
|
const allOrderNumbers: string[] = []
|
||||||
|
|
||||||
@@ -71,6 +71,22 @@ export async function getSourceNumbersFromInputs(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
orderNumbers.push(...allOrderNumbers)
|
||||||
|
} else if (dbType === 'postgresql') {
|
||||||
|
const allOrderNumbers: string[] = []
|
||||||
|
|
||||||
|
for (let i = 0; i < productionIds.length; i += batchSize) {
|
||||||
|
const batch = productionIds.slice(i, i + batchSize)
|
||||||
|
const placeholders = batch.map((_, idx) => `$${idx + 1}`).join(',')
|
||||||
|
const contractSql = `
|
||||||
|
SELECT DISTINCT "生产订单号"
|
||||||
|
FROM ${contractTableName}
|
||||||
|
WHERE "总排号" IN (${placeholders})
|
||||||
|
`
|
||||||
|
const contractResult = await dbService.query(contractSql, batch)
|
||||||
|
allOrderNumbers.push(...contractResult.rows.map((row) => row.生产订单号 as string))
|
||||||
|
}
|
||||||
|
|
||||||
orderNumbers.push(...allOrderNumbers)
|
orderNumbers.push(...allOrderNumbers)
|
||||||
} else {
|
} else {
|
||||||
const allOrderNumbers: string[] = []
|
const allOrderNumbers: string[] = []
|
||||||
|
|||||||
@@ -463,6 +463,18 @@ export class ValidationApplicationService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dbService.type === 'postgresql') {
|
||||||
|
return dbService.query(
|
||||||
|
`
|
||||||
|
SELECT "MaterialName", "Specification", "Model"
|
||||||
|
FROM ${detailTableName}
|
||||||
|
WHERE "MaterialCode" = $1
|
||||||
|
LIMIT 1
|
||||||
|
`,
|
||||||
|
[materialCode]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return dbService.query(
|
return dbService.query(
|
||||||
`
|
`
|
||||||
SELECT MaterialName, Specification, Model
|
SELECT MaterialName, Specification, Model
|
||||||
@@ -521,6 +533,24 @@ export class ValidationApplicationService {
|
|||||||
return materialCodes
|
return materialCodes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dbService.type === 'postgresql') {
|
||||||
|
const result = await dbService.query(
|
||||||
|
`
|
||||||
|
SELECT "MaterialCode"
|
||||||
|
FROM ${markedTableName}
|
||||||
|
WHERE "ManagerName" = $1 AND "MaterialCode" IS NOT NULL
|
||||||
|
`,
|
||||||
|
[username]
|
||||||
|
)
|
||||||
|
const materialCodes = result.rows.map((row) => row.MaterialCode as string).filter(Boolean)
|
||||||
|
log.info(`Regular user: got ${materialCodes.length} materials`, {
|
||||||
|
userId: username,
|
||||||
|
isAdmin: false,
|
||||||
|
materialCount: materialCodes.length
|
||||||
|
})
|
||||||
|
return materialCodes
|
||||||
|
}
|
||||||
|
|
||||||
const result = await dbService.query(
|
const result = await dbService.query(
|
||||||
`
|
`
|
||||||
SELECT MaterialCode
|
SELECT MaterialCode
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { ConfigManager } from '../config/config-manager'
|
import { ConfigManager } from '../config/config-manager'
|
||||||
import { MySqlService } from '../database/mysql'
|
import { MySqlService } from '../database/mysql'
|
||||||
import { SqlServerService } from '../database/sql-server'
|
import { SqlServerService } from '../database/sql-server'
|
||||||
|
import { PostgreSqlService } from '../database/postgresql'
|
||||||
|
|
||||||
export type ValidationDatabaseService = MySqlService | SqlServerService
|
export type ValidationDatabaseService = MySqlService | SqlServerService | PostgreSqlService
|
||||||
|
|
||||||
export async function createValidationDatabaseService(): Promise<ValidationDatabaseService> {
|
export async function createValidationDatabaseService(): Promise<ValidationDatabaseService> {
|
||||||
const configManager = ConfigManager.getInstance()
|
const configManager = ConfigManager.getInstance()
|
||||||
@@ -26,6 +27,19 @@ export async function createValidationDatabaseService(): Promise<ValidationDatab
|
|||||||
return sqlServerService
|
return sqlServerService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dbType === 'postgresql') {
|
||||||
|
const dbConfig = config.database.postgresql
|
||||||
|
const pgService = new PostgreSqlService({
|
||||||
|
host: dbConfig.host,
|
||||||
|
port: dbConfig.port,
|
||||||
|
user: dbConfig.username,
|
||||||
|
password: dbConfig.password,
|
||||||
|
database: dbConfig.database
|
||||||
|
})
|
||||||
|
await pgService.connect()
|
||||||
|
return pgService
|
||||||
|
}
|
||||||
|
|
||||||
const dbConfig = config.database.mysql
|
const dbConfig = config.database.mysql
|
||||||
const mysqlService = new MySqlService({
|
const mysqlService = new MySqlService({
|
||||||
host: dbConfig.host,
|
host: dbConfig.host,
|
||||||
@@ -42,15 +56,21 @@ export function getValidationTableName(mysqlTableName: string): string {
|
|||||||
const configManager = ConfigManager.getInstance()
|
const configManager = ConfigManager.getInstance()
|
||||||
const dbType = configManager.getDatabaseType()
|
const dbType = configManager.getDatabaseType()
|
||||||
|
|
||||||
if (dbType === 'sqlserver') {
|
if (dbType === 'sqlserver' || dbType === 'postgresql') {
|
||||||
const firstUnderscoreIndex = mysqlTableName.indexOf('_')
|
const firstUnderscoreIndex = mysqlTableName.indexOf('_')
|
||||||
if (firstUnderscoreIndex > 0) {
|
if (firstUnderscoreIndex > 0) {
|
||||||
const schema = mysqlTableName.substring(0, firstUnderscoreIndex)
|
const schema = mysqlTableName.substring(0, firstUnderscoreIndex)
|
||||||
const tableName = mysqlTableName.substring(firstUnderscoreIndex + 1)
|
const tableName = mysqlTableName.substring(firstUnderscoreIndex + 1)
|
||||||
|
if (dbType === 'sqlserver') {
|
||||||
return `[${schema}].[${tableName}]`
|
return `[${schema}].[${tableName}]`
|
||||||
}
|
}
|
||||||
|
return `"${schema}"."${tableName}"`
|
||||||
|
}
|
||||||
|
if (dbType === 'sqlserver') {
|
||||||
return `[dbo].[${mysqlTableName}]`
|
return `[dbo].[${mysqlTableName}]`
|
||||||
}
|
}
|
||||||
|
return `"public"."${mysqlTableName}"`
|
||||||
|
}
|
||||||
|
|
||||||
return mysqlTableName
|
return mysqlTableName
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,4 +206,225 @@ describe('prepareSql', () => {
|
|||||||
expect(result).toContain('as count')
|
expect(result).toContain('as count')
|
||||||
expect(result).toContain('"UserName"')
|
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"')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user