@javalabs/prisma-client 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/scripts/data-migration/batch-migrator.d.ts +14 -19
- package/dist/scripts/data-migration/batch-migrator.js +98 -297
- package/dist/scripts/data-migration/batch-migrator.js.map +1 -1
- package/dist/scripts/data-migration/data-transformer.d.ts +16 -7
- package/dist/scripts/data-migration/data-transformer.js +169 -133
- package/dist/scripts/data-migration/data-transformer.js.map +1 -1
- package/dist/scripts/data-migration/db-connector.d.ts +6 -1
- package/dist/scripts/data-migration/db-connector.js +44 -8
- package/dist/scripts/data-migration/db-connector.js.map +1 -1
- package/dist/scripts/data-migration/dependency-resolver.d.ts +10 -10
- package/dist/scripts/data-migration/dependency-resolver.js +92 -211
- package/dist/scripts/data-migration/dependency-resolver.js.map +1 -1
- package/dist/scripts/data-migration/foreign-key-manager.d.ts +6 -5
- package/dist/scripts/data-migration/foreign-key-manager.js +108 -18
- package/dist/scripts/data-migration/foreign-key-manager.js.map +1 -1
- package/dist/scripts/data-migration/migration-config.json +63 -0
- package/dist/scripts/data-migration/migration-tool.d.ts +25 -6
- package/dist/scripts/data-migration/migration-tool.js +78 -38
- package/dist/scripts/data-migration/migration-tool.js.map +1 -1
- package/dist/scripts/data-migration/multi-source-migrator.d.ts +17 -0
- package/dist/scripts/data-migration/multi-source-migrator.js +130 -0
- package/dist/scripts/data-migration/multi-source-migrator.js.map +1 -0
- package/dist/scripts/data-migration/schema-utils.d.ts +3 -3
- package/dist/scripts/data-migration/schema-utils.js +62 -19
- package/dist/scripts/data-migration/schema-utils.js.map +1 -1
- package/dist/scripts/data-migration/tenant-migrator.js +9 -2
- package/dist/scripts/data-migration/tenant-migrator.js.map +1 -1
- package/dist/scripts/data-migration/typecast-manager.d.ts +7 -3
- package/dist/scripts/data-migration/typecast-manager.js +169 -25
- package/dist/scripts/data-migration/typecast-manager.js.map +1 -1
- package/dist/scripts/data-migration/types.d.ts +68 -2
- package/dist/scripts/fix-table-indexes.d.ts +26 -0
- package/dist/scripts/fix-table-indexes.js +460 -0
- package/dist/scripts/fix-table-indexes.js.map +1 -0
- package/dist/scripts/multi-db-migration.d.ts +1 -0
- package/dist/scripts/multi-db-migration.js +55 -0
- package/dist/scripts/multi-db-migration.js.map +1 -0
- package/dist/scripts/run-migration.js +41 -75
- package/dist/scripts/run-migration.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/migration-config.json +40 -72
- package/{migration-config-public.json → migration-config.json.bk} +14 -14
- package/package.json +6 -3
- package/src/scripts/data-migration/batch-migrator.ts +192 -513
- package/src/scripts/data-migration/data-transformer.ts +252 -203
- package/src/scripts/data-migration/db-connector.ts +66 -13
- package/src/scripts/data-migration/dependency-resolver.ts +121 -266
- package/src/scripts/data-migration/foreign-key-manager.ts +214 -32
- package/src/scripts/data-migration/migration-config.json +63 -0
- package/src/scripts/data-migration/migration-tool.ts +377 -225
- package/src/scripts/data-migration/schema-utils.ts +94 -32
- package/src/scripts/data-migration/tenant-migrator.ts +12 -5
- package/src/scripts/data-migration/typecast-manager.ts +186 -31
- package/src/scripts/data-migration/types.ts +78 -5
- package/src/scripts/dumps/source_dump_20250428_145606.sql +323 -0
- package/src/scripts/fix-table-indexes.ts +602 -0
- package/src/scripts/post-migration-validator.ts +206 -107
- package/src/scripts/run-migration.ts +87 -101
|
@@ -1,569 +1,248 @@
|
|
|
1
1
|
import { Logger } from "@nestjs/common";
|
|
2
|
-
import { PrismaClient } from "@prisma/client";
|
|
3
|
-
import { EntityType, ColumnSchema, EnumCastValue } from "./types";
|
|
4
|
-
import { DataTransformer } from "./data-transformer";
|
|
5
2
|
import { SchemaUtils } from "./schema-utils";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
3
|
+
import { DataTransformer } from "./data-transformer";
|
|
4
|
+
import { TypecastManager } from "./typecast-manager";
|
|
5
|
+
import {
|
|
6
|
+
ColumnSchema,
|
|
7
|
+
DatabaseConnections,
|
|
8
|
+
MigrationOptions,
|
|
9
|
+
TableConfig,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
const BATCH_SIZE = 100;
|
|
13
|
+
const MAX_RETRIES = 3;
|
|
14
|
+
const RETRY_BASE_DELAY = 1000;
|
|
9
15
|
|
|
10
16
|
export class BatchMigrator {
|
|
11
17
|
private readonly logger = new Logger("BatchMigrator");
|
|
12
|
-
private readonly
|
|
13
|
-
// Removed typecastManager as it wasn't used and casting is handled inline
|
|
18
|
+
private readonly typecastManager: TypecastManager;
|
|
14
19
|
|
|
15
20
|
constructor(
|
|
16
|
-
private readonly dataTransformer: DataTransformer,
|
|
17
21
|
private readonly schemaUtils: SchemaUtils,
|
|
22
|
+
private readonly dataTransformer: DataTransformer,
|
|
18
23
|
private readonly connections: DatabaseConnections,
|
|
19
|
-
private readonly
|
|
20
|
-
private readonly
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
private readonly options: MigrationOptions,
|
|
25
|
+
private readonly providerId?: string | null
|
|
26
|
+
) {
|
|
27
|
+
this.typecastManager = new TypecastManager();
|
|
28
|
+
}
|
|
23
29
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
): Promise<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
`SELECT EXISTS (SELECT 1 FROM "${tenantId}"."${tableName}" LIMIT 1)`
|
|
32
|
-
);
|
|
33
|
-
return result.rows[0]?.exists || false;
|
|
34
|
-
} catch (error) {
|
|
35
|
-
this.logger.warn(
|
|
36
|
-
`Error checking data existence for ${tableName}: ${error.message}`
|
|
30
|
+
async validateSchema(
|
|
31
|
+
sourceColumns: ColumnSchema[],
|
|
32
|
+
targetColumns: ColumnSchema[]
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
for (const targetColumn of targetColumns) {
|
|
35
|
+
const sourceColumn = sourceColumns.find(
|
|
36
|
+
(col) => col.column_name === targetColumn.column_name
|
|
37
37
|
);
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
38
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
39
|
+
if (!sourceColumn) {
|
|
40
|
+
if (targetColumn.is_nullable === "NO") {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Required column ${targetColumn.column_name} not found in source schema`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
this.logger.warn(
|
|
46
|
+
`Column ${targetColumn.column_name} not found in source schema but is nullable`
|
|
47
|
+
);
|
|
48
|
+
continue;
|
|
52
49
|
}
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
if (
|
|
52
|
+
!this.typecastManager.areTypesCompatible(
|
|
53
|
+
sourceColumn.data_type,
|
|
54
|
+
targetColumn.data_type
|
|
55
|
+
)
|
|
56
|
+
) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Incompatible data types for column ${targetColumn.column_name}: ` +
|
|
59
|
+
`source ${sourceColumn.data_type} -> target ${targetColumn.data_type}`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
58
64
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
async migrateEntityDataInBatches(
|
|
66
|
+
sourceSchema: string,
|
|
67
|
+
targetSchema: string,
|
|
68
|
+
tableConfig: TableConfig,
|
|
69
|
+
tenantId: string
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
const sourceColumns = await this.schemaUtils.getTableColumns(
|
|
72
|
+
sourceSchema,
|
|
73
|
+
tableConfig.sourceTable
|
|
74
|
+
);
|
|
75
|
+
const targetColumns = await this.schemaUtils.getTableColumns(
|
|
76
|
+
targetSchema,
|
|
77
|
+
tableConfig.targetTable
|
|
78
|
+
);
|
|
64
79
|
|
|
65
|
-
|
|
80
|
+
await this.validateSchema(sourceColumns, targetColumns);
|
|
66
81
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
? `Filtering source by Provider ID: ${providerId}`
|
|
71
|
-
: "Migrating all source records."
|
|
72
|
-
}`
|
|
73
|
-
);
|
|
82
|
+
let offset = 0;
|
|
83
|
+
let hasMoreRecords = true;
|
|
84
|
+
let retryCount = 0;
|
|
74
85
|
|
|
86
|
+
while (hasMoreRecords) {
|
|
75
87
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
88
|
+
const records = await this.fetchBatch(
|
|
89
|
+
sourceSchema,
|
|
90
|
+
tableConfig,
|
|
91
|
+
offset,
|
|
92
|
+
BATCH_SIZE
|
|
81
93
|
);
|
|
82
94
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
`Source table ${tableName} schema not found. Skipping.`
|
|
87
|
-
);
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
if (!targetTableSchema.length) {
|
|
91
|
-
this.logger.warn(
|
|
92
|
-
`Target table ${tableName} schema '${targetSchema}' not found. Skipping.`
|
|
93
|
-
);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// --- DYNAMIC SOURCE QUERY GENERATION ---
|
|
98
|
-
let selectQuery: string;
|
|
99
|
-
let queryParams: any[] = [];
|
|
100
|
-
|
|
101
|
-
// Base query selects all columns from the source table
|
|
102
|
-
// Using alias 't' for the primary table
|
|
103
|
-
const columnList = sourceTableSchema
|
|
104
|
-
.map((col) => `"${col.column_name}"`)
|
|
105
|
-
.join(", ");
|
|
106
|
-
let fromClause = `FROM "${tableName}" t`;
|
|
107
|
-
let whereClause = "";
|
|
108
|
-
|
|
109
|
-
if (providerId && filterColumn) {
|
|
110
|
-
// Filtering is needed
|
|
111
|
-
if (filterVia) {
|
|
112
|
-
// Filter through an intermediate table (JOIN needed)
|
|
113
|
-
// Assumes intermediate table links via its primary key (e.g., 'id') to filterColumn
|
|
114
|
-
// Assumes intermediate table links to providers via 'provider_id'
|
|
115
|
-
// Example: Migrating 'invoices' (t) via 'transactions' (j)
|
|
116
|
-
// Needs filterColumn='transaction_id' (linking t to j) and filterVia='transactions'
|
|
117
|
-
// Final filter is j.provider_id = $1
|
|
118
|
-
// NOTE: This makes assumptions! A more robust solution might need more config.
|
|
119
|
-
// Let's assume filterVia table has a primary key named 'id' and a 'provider_id' column
|
|
120
|
-
const joinTable = filterVia;
|
|
121
|
-
const joinCondition = `t."${filterColumn}" = j.id`; // Assuming PK of joinTable is 'id'
|
|
122
|
-
const providerFilter = `j.provider_id = $1`; // Assuming FK in joinTable is 'provider_id'
|
|
123
|
-
|
|
124
|
-
fromClause = `FROM "${tableName}" t JOIN "${joinTable}" j ON ${joinCondition}`;
|
|
125
|
-
whereClause = `WHERE ${providerFilter}`;
|
|
126
|
-
queryParams = [providerId];
|
|
127
|
-
this.logger.log(`Using JOIN filter: ${fromClause} ${whereClause}`);
|
|
128
|
-
} else {
|
|
129
|
-
// Direct filter on the table itself
|
|
130
|
-
whereClause = `WHERE t."${filterColumn}" = $1`;
|
|
131
|
-
queryParams = [providerId];
|
|
132
|
-
this.logger.log(`Using direct filter: ${whereClause}`);
|
|
133
|
-
}
|
|
134
|
-
} else {
|
|
135
|
-
this.logger.log(
|
|
136
|
-
`No providerId filter applied for ${tableName}. Selecting all records.`
|
|
137
|
-
);
|
|
138
|
-
// No filtering needed (e.g., migrating public tables or filteredPublic without providerId)
|
|
95
|
+
if (records.length === 0) {
|
|
96
|
+
hasMoreRecords = false;
|
|
97
|
+
continue;
|
|
139
98
|
}
|
|
140
99
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
);
|
|
149
|
-
const totalRecords = sourceData.rows.length;
|
|
150
|
-
|
|
151
|
-
this.logger.log(
|
|
152
|
-
`Found ${totalRecords} ${tableName} records in source to migrate to '${targetSchema}'.`
|
|
100
|
+
await this.processBatchWithTransaction(
|
|
101
|
+
records,
|
|
102
|
+
targetSchema,
|
|
103
|
+
tableConfig,
|
|
104
|
+
sourceColumns,
|
|
105
|
+
targetColumns,
|
|
106
|
+
tenantId
|
|
153
107
|
);
|
|
154
108
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
109
|
+
offset += BATCH_SIZE;
|
|
110
|
+
retryCount = 0; // Reset retry count on success
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (retryCount < MAX_RETRIES) {
|
|
113
|
+
retryCount++;
|
|
114
|
+
const delay = RETRY_BASE_DELAY * Math.pow(2, retryCount - 1);
|
|
115
|
+
this.logger.warn(
|
|
116
|
+
`Error processing batch, retrying in ${delay}ms (attempt ${retryCount}/${MAX_RETRIES}): ${error.message}`
|
|
158
117
|
);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
118
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
119
|
+
} else {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`Failed to process batch after ${MAX_RETRIES} retries: ${error.message}`
|
|
162
122
|
);
|
|
163
|
-
return;
|
|
164
123
|
}
|
|
165
|
-
|
|
166
|
-
// Use the specific idField from the entity config
|
|
167
|
-
const primaryKeyField = idField;
|
|
168
|
-
|
|
169
|
-
await this.processRecords(
|
|
170
|
-
prisma, // Pass the main Prisma client
|
|
171
|
-
targetSchema, // Pass the specific target schema for insertion
|
|
172
|
-
tableName,
|
|
173
|
-
primaryKeyField,
|
|
174
|
-
sourceData.rows,
|
|
175
|
-
sourceTableSchema,
|
|
176
|
-
targetTableSchema
|
|
177
|
-
);
|
|
178
|
-
} catch (error) {
|
|
179
|
-
this.logger.error(
|
|
180
|
-
`Error during migration step for ${tableName} to schema '${targetSchema}': ${error.message}`
|
|
181
|
-
);
|
|
182
|
-
// Log context for better debugging
|
|
183
|
-
console.error(`Entity Config:`, JSON.stringify(entity));
|
|
184
|
-
console.error(`Provider ID used for filter:`, providerId);
|
|
185
|
-
// console.error(`Generated Select Query:`, selectQuery); // selectQuery might be out of scope here
|
|
186
|
-
// console.error(`Query Params:`, JSON.stringify(queryParams)); // queryParams might be out of scope here
|
|
187
|
-
// Rethrow to be caught by the outer try/catch in DataMigrationTool
|
|
188
|
-
throw error;
|
|
189
|
-
}
|
|
190
|
-
} catch (error) {
|
|
191
|
-
// Catch errors from initial checks or schema operations
|
|
192
|
-
this.logger.error(
|
|
193
|
-
`Error preparing migration for ${entity.name} to schema '${targetSchema}': ${error.message}`
|
|
194
|
-
);
|
|
195
|
-
throw error; // Rethrow to be caught by DataMigrationTool
|
|
196
|
-
} finally {
|
|
197
|
-
// ALWAYS re-enable foreign key checks, even if errors occurred
|
|
198
|
-
try {
|
|
199
|
-
await this.connections.targetPool.query(
|
|
200
|
-
`SET session_replication_role = 'origin';`
|
|
201
|
-
);
|
|
202
|
-
} catch (finallyError) {
|
|
203
|
-
this.logger.error(
|
|
204
|
-
`Failed to reset session_replication_role: ${finallyError.message}`
|
|
205
|
-
);
|
|
206
124
|
}
|
|
207
125
|
}
|
|
208
126
|
}
|
|
209
127
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
// Optional: Baseline migration record for the new schema if using Prisma Migrate
|
|
228
|
-
try {
|
|
229
|
-
await this.connections.targetPool.query(`
|
|
230
|
-
INSERT INTO "${schemaName}"."_prisma_migrations" (id, checksum, finished_at, migration_name, logs, rolled_back_at, started_at, applied_steps_count)
|
|
231
|
-
SELECT id, checksum, finished_at, migration_name, logs, rolled_back_at, started_at, applied_steps_count
|
|
232
|
-
FROM "public"."_prisma_migrations" WHERE migration_name LIKE '%_init'
|
|
233
|
-
ON CONFLICT DO NOTHING;
|
|
234
|
-
`);
|
|
235
|
-
this.logger.log(
|
|
236
|
-
`Attempted to copy initial migration record to schema '${schemaName}'.`
|
|
237
|
-
);
|
|
238
|
-
} catch (migrationError) {
|
|
239
|
-
this.logger.warn(
|
|
240
|
-
`Could not copy baseline migration to schema '${schemaName}': ${migrationError.message}`
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
128
|
+
private async fetchBatch(
|
|
129
|
+
sourceSchema: string,
|
|
130
|
+
tableConfig: TableConfig,
|
|
131
|
+
offset: number,
|
|
132
|
+
limit: number
|
|
133
|
+
): Promise<any[]> {
|
|
134
|
+
const query = `
|
|
135
|
+
SELECT *
|
|
136
|
+
FROM "${sourceSchema}"."${tableConfig.sourceTable}"
|
|
137
|
+
${this.buildWhereClause(tableConfig)}
|
|
138
|
+
ORDER BY "${tableConfig.idField}"
|
|
139
|
+
LIMIT ${limit}
|
|
140
|
+
OFFSET ${offset}
|
|
141
|
+
`;
|
|
142
|
+
|
|
143
|
+
const result = await this.connections.sourcePool.query(query);
|
|
144
|
+
return result.rows;
|
|
244
145
|
}
|
|
245
146
|
|
|
246
|
-
private
|
|
247
|
-
const
|
|
248
|
-
if (!this.schemaCache[cacheKey]) {
|
|
249
|
-
this.logger.debug(`Cache miss for source schema: ${tableName}`);
|
|
250
|
-
this.schemaCache[cacheKey] = await this.schemaUtils.getTableSchema(
|
|
251
|
-
tableName,
|
|
252
|
-
"source",
|
|
253
|
-
"public" // Source is always public schema in this context
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
return this.schemaCache[cacheKey];
|
|
257
|
-
}
|
|
147
|
+
private buildWhereClause(tableConfig: TableConfig): string {
|
|
148
|
+
const conditions = [];
|
|
258
149
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
tableName: string
|
|
262
|
-
): Promise<ColumnSchema[]> {
|
|
263
|
-
const cacheKey = `${schema}.${tableName}`;
|
|
264
|
-
if (!this.targetSchemaCache[cacheKey]) {
|
|
265
|
-
this.logger.debug(`Cache miss for target schema: ${cacheKey}`);
|
|
266
|
-
this.targetSchemaCache[cacheKey] = await this.schemaUtils.getTableSchema(
|
|
267
|
-
tableName,
|
|
268
|
-
"target",
|
|
269
|
-
schema // Use the provided target schema name
|
|
270
|
-
);
|
|
150
|
+
if (this.providerId) {
|
|
151
|
+
conditions.push(`"${tableConfig.providerLink}" = '${this.providerId}'`);
|
|
271
152
|
}
|
|
272
|
-
return this.targetSchemaCache[cacheKey];
|
|
273
|
-
}
|
|
274
153
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
this.logger.debug(
|
|
278
|
-
`Executing source query: ${query.replace(
|
|
279
|
-
/\s\s+/g,
|
|
280
|
-
" "
|
|
281
|
-
)} || PARAMS: ${JSON.stringify(params)}`
|
|
282
|
-
);
|
|
283
|
-
const result = await this.connections.sourcePool.query(query, params);
|
|
284
|
-
this.logger.debug(`Query returned ${result.rows?.length || 0} rows`);
|
|
285
|
-
return result;
|
|
286
|
-
} catch (error) {
|
|
287
|
-
this.logger.error(`Error executing source query: ${error.message}`);
|
|
288
|
-
this.logger.error(`Query was: ${query}`);
|
|
289
|
-
this.logger.error(`Params were: ${JSON.stringify(params)}`);
|
|
290
|
-
throw error;
|
|
154
|
+
if (tableConfig.filterColumn) {
|
|
155
|
+
conditions.push(`"${tableConfig.filterColumn}" IS NOT NULL`);
|
|
291
156
|
}
|
|
292
|
-
}
|
|
293
157
|
|
|
294
|
-
|
|
295
|
-
schemaName: string,
|
|
296
|
-
tableName: string
|
|
297
|
-
): Promise<string | null> {
|
|
298
|
-
// This function remains useful for verifying the actual PK if needed,
|
|
299
|
-
// but we primarily rely on the idField from the config now.
|
|
300
|
-
try {
|
|
301
|
-
const result = await this.connections.targetPool.query(
|
|
302
|
-
`
|
|
303
|
-
SELECT kcu.column_name
|
|
304
|
-
FROM information_schema.table_constraints tc
|
|
305
|
-
JOIN information_schema.key_column_usage kcu
|
|
306
|
-
ON tc.constraint_name = kcu.constraint_name
|
|
307
|
-
AND tc.table_schema = kcu.table_schema
|
|
308
|
-
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
309
|
-
AND tc.table_schema = $1
|
|
310
|
-
AND tc.table_name = $2
|
|
311
|
-
`,
|
|
312
|
-
[schemaName, tableName]
|
|
313
|
-
);
|
|
314
|
-
return result.rows[0]?.column_name || null;
|
|
315
|
-
} catch (error) {
|
|
316
|
-
this.logger.error(
|
|
317
|
-
`Error getting primary key for ${schemaName}.${tableName}: ${error.message}`
|
|
318
|
-
);
|
|
319
|
-
return null;
|
|
320
|
-
}
|
|
158
|
+
return conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
321
159
|
}
|
|
322
160
|
|
|
323
|
-
|
|
324
|
-
// This function needs significant changes to correctly handle the targetSchema
|
|
325
|
-
|
|
326
|
-
private async processRecords(
|
|
327
|
-
prisma: PrismaClient, // Main prisma client
|
|
328
|
-
targetSchema: string, // The ACTUAL schema to insert/update into
|
|
329
|
-
tableName: string,
|
|
330
|
-
idField: string, // Use idField from config
|
|
161
|
+
private async processBatchWithTransaction(
|
|
331
162
|
records: any[],
|
|
332
|
-
|
|
333
|
-
|
|
163
|
+
targetSchema: string,
|
|
164
|
+
tableConfig: TableConfig,
|
|
165
|
+
sourceColumns: ColumnSchema[],
|
|
166
|
+
targetColumns: ColumnSchema[],
|
|
167
|
+
tenantId: string
|
|
334
168
|
): Promise<void> {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
);
|
|
339
|
-
return; // Or throw error
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
for (const record of records) {
|
|
343
|
-
let recordId = record[idField];
|
|
344
|
-
try {
|
|
345
|
-
this.logger.debug(
|
|
346
|
-
`Processing record ${
|
|
347
|
-
recordId ?? "(no id found)"
|
|
348
|
-
} for ${targetSchema}.${tableName}`
|
|
349
|
-
);
|
|
350
|
-
|
|
351
|
-
if (!record || Object.keys(record).length === 0) {
|
|
352
|
-
this.logger.warn(`Empty record found for ${tableName}, skipping`);
|
|
353
|
-
continue;
|
|
354
|
-
}
|
|
355
|
-
if (!recordId) {
|
|
356
|
-
this.logger.warn(
|
|
357
|
-
`Record missing configured ID field '${idField}' in source data, skipping: ${JSON.stringify(
|
|
358
|
-
record
|
|
359
|
-
)}`
|
|
360
|
-
);
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
169
|
+
const client = await this.connections.targetPool.connect();
|
|
170
|
+
try {
|
|
171
|
+
await client.query("BEGIN");
|
|
363
172
|
|
|
364
|
-
|
|
365
|
-
const
|
|
173
|
+
for (const record of records) {
|
|
174
|
+
const transformedRecord = await this.transformRecord(
|
|
366
175
|
record,
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
176
|
+
sourceColumns,
|
|
177
|
+
targetColumns,
|
|
178
|
+
tenantId
|
|
370
179
|
);
|
|
371
180
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
);
|
|
378
|
-
if (!columnSchema) return acc; // Skip columns not in target schema
|
|
379
|
-
|
|
380
|
-
const columnName = `"${columnSchema.column_name}"`; // Quote column names
|
|
381
|
-
|
|
382
|
-
// --- Helper function to escape values for SQL E'' strings ---
|
|
383
|
-
const escapeValue = (val: any): string => {
|
|
384
|
-
if (val === null || val === undefined) return "NULL";
|
|
385
|
-
if (typeof val === "boolean") return val ? "TRUE" : "FALSE";
|
|
386
|
-
if (typeof val === "number") return String(val);
|
|
387
|
-
// Escape single quotes and backslashes for E'' syntax
|
|
388
|
-
return String(val).replace(/'/g, "''").replace(/\\/g, "");
|
|
389
|
-
};
|
|
390
|
-
// --- End escapeValue ---
|
|
391
|
-
|
|
392
|
-
if (value === null || value === undefined) {
|
|
393
|
-
acc[columnName] = "NULL";
|
|
394
|
-
return acc;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Special handling for EnumCastValue objects from transformer
|
|
398
|
-
if (
|
|
399
|
-
typeof value === "object" &&
|
|
400
|
-
value !== null &&
|
|
401
|
-
value["needsEnumCast"]
|
|
402
|
-
) {
|
|
403
|
-
const enumValue = value as EnumCastValue;
|
|
404
|
-
const schemaPrefix =
|
|
405
|
-
targetSchema === "public" ? '"public".' : `"${targetSchema}".`;
|
|
406
|
-
const quotedEnumType = `"${enumValue.enumType}"`;
|
|
407
|
-
const escapedEnumValue = escapeValue(enumValue.value);
|
|
408
|
-
// Enum values are typically strings, use E''
|
|
409
|
-
acc[
|
|
410
|
-
columnName
|
|
411
|
-
] = `CAST(E'${escapedEnumValue}' AS ${schemaPrefix}${quotedEnumType})`;
|
|
412
|
-
return acc;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Handle standard types - Use data_type primarily, fallback to udt_name for enums/user-defined
|
|
416
|
-
let targetType = columnSchema.data_type.toLowerCase();
|
|
417
|
-
let udtName = columnSchema.udt_name;
|
|
418
|
-
let sqlValue: string;
|
|
419
|
-
let requiresQuotes = false;
|
|
420
|
-
|
|
421
|
-
// Determine if quotes are needed based on type category
|
|
422
|
-
if (
|
|
423
|
-
[
|
|
424
|
-
"text",
|
|
425
|
-
"varchar",
|
|
426
|
-
"character varying",
|
|
427
|
-
"char",
|
|
428
|
-
"timestamp with time zone",
|
|
429
|
-
"timestamptz",
|
|
430
|
-
"timestamp without time zone",
|
|
431
|
-
"timestamp",
|
|
432
|
-
"date",
|
|
433
|
-
"uuid",
|
|
434
|
-
"json",
|
|
435
|
-
"jsonb",
|
|
436
|
-
].includes(targetType) ||
|
|
437
|
-
targetType.includes("enum") ||
|
|
438
|
-
(targetType === "user-defined" && udtName?.startsWith("enum_"))
|
|
439
|
-
) {
|
|
440
|
-
requiresQuotes = true;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Escape the value appropriately
|
|
444
|
-
const escaped = escapeValue(value);
|
|
445
|
-
if (escaped === "NULL") {
|
|
446
|
-
// Use SQL NULL keyword directly
|
|
447
|
-
sqlValue = "NULL";
|
|
448
|
-
} else if (requiresQuotes) {
|
|
449
|
-
sqlValue = `E'${escaped}'`; // Use E'' for strings, dates, enums, json, etc.
|
|
450
|
-
} else {
|
|
451
|
-
sqlValue = escaped; // Use raw value for numbers, booleans
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Determine necessary casting based on target type
|
|
455
|
-
let castExpression = "";
|
|
456
|
-
if (targetType.includes("timestamp"))
|
|
457
|
-
castExpression = "::timestamp with time zone";
|
|
458
|
-
else if (targetType === "date") castExpression = "::date";
|
|
459
|
-
else if (
|
|
460
|
-
targetType === "integer" ||
|
|
461
|
-
targetType === "int" ||
|
|
462
|
-
targetType === "int4"
|
|
463
|
-
)
|
|
464
|
-
castExpression = "::integer";
|
|
465
|
-
else if (targetType === "bigint" || targetType === "int8")
|
|
466
|
-
castExpression = "::bigint";
|
|
467
|
-
else if (targetType === "smallint" || targetType === "int2")
|
|
468
|
-
castExpression = "::smallint";
|
|
469
|
-
else if (targetType === "numeric" || targetType === "decimal")
|
|
470
|
-
castExpression = "::numeric";
|
|
471
|
-
else if (targetType === "real" || targetType === "float4")
|
|
472
|
-
castExpression = "::real";
|
|
473
|
-
else if (
|
|
474
|
-
targetType === "double precision" ||
|
|
475
|
-
targetType === "float8"
|
|
476
|
-
)
|
|
477
|
-
castExpression = "::double precision";
|
|
478
|
-
else if (targetType === "boolean" || targetType === "bool")
|
|
479
|
-
castExpression = "::boolean";
|
|
480
|
-
else if (targetType === "json" || targetType === "jsonb")
|
|
481
|
-
castExpression = `::${targetType}`;
|
|
482
|
-
else if (targetType === "uuid") castExpression = "::uuid";
|
|
483
|
-
else if (targetType === "text" || targetType.includes("char"))
|
|
484
|
-
castExpression = "::text";
|
|
485
|
-
else if (
|
|
486
|
-
targetType === "user-defined" &&
|
|
487
|
-
udtName?.startsWith("enum_")
|
|
488
|
-
) {
|
|
489
|
-
const schemaPrefix =
|
|
490
|
-
targetSchema === "public" ? '"public".' : `"${targetSchema}".`;
|
|
491
|
-
castExpression = `::${schemaPrefix}"${udtName}"`;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
acc[columnName] = `${sqlValue}${castExpression}`;
|
|
495
|
-
|
|
496
|
-
return acc;
|
|
497
|
-
},
|
|
498
|
-
{} as Record<string, string> // Accumulator holds SQL value strings
|
|
499
|
-
);
|
|
500
|
-
|
|
501
|
-
// Filter out entries where processedData might be undefined/invalid if needed
|
|
502
|
-
const validProcessedData = Object.entries(processedData).reduce(
|
|
503
|
-
(acc, [key, val]) => {
|
|
504
|
-
if (
|
|
505
|
-
val !== undefined &&
|
|
506
|
-
val !== "NULL" &&
|
|
507
|
-
val !== "E''" &&
|
|
508
|
-
val !== "E'undefined'" &&
|
|
509
|
-
val !== "E'null'"
|
|
510
|
-
) {
|
|
511
|
-
// Additional checks for empty/invalid strings
|
|
512
|
-
acc[key] = val;
|
|
513
|
-
}
|
|
514
|
-
return acc;
|
|
515
|
-
},
|
|
516
|
-
{}
|
|
181
|
+
await this.insertRecord(
|
|
182
|
+
client,
|
|
183
|
+
targetSchema,
|
|
184
|
+
tableConfig.targetTable,
|
|
185
|
+
transformedRecord
|
|
517
186
|
);
|
|
187
|
+
}
|
|
518
188
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Construct the SET clause for UPDATE
|
|
530
|
-
const updateSetClauses = columns
|
|
531
|
-
.filter((col) => col !== `"${idField}"`) // Don't update the PK itself
|
|
532
|
-
.map((col) => `${col} = EXCLUDED.${col}`) // Use EXCLUDED to get the value proposed for insertion
|
|
533
|
-
.join(", ");
|
|
534
|
-
|
|
535
|
-
// Ensure target schema and table name are quoted
|
|
536
|
-
const quotedSchemaTable = `"${targetSchema}"."${tableName}"`;
|
|
537
|
-
const quotedIdField = `"${idField}"`;
|
|
189
|
+
await client.query("COMMIT");
|
|
190
|
+
} catch (error) {
|
|
191
|
+
await client.query("ROLLBACK");
|
|
192
|
+
throw error;
|
|
193
|
+
} finally {
|
|
194
|
+
client.release();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
538
197
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
198
|
+
private async transformRecord(
|
|
199
|
+
record: any,
|
|
200
|
+
sourceColumns: ColumnSchema[],
|
|
201
|
+
targetColumns: ColumnSchema[],
|
|
202
|
+
tenantId: string
|
|
203
|
+
): Promise<any> {
|
|
204
|
+
const transformedRecord: any = {};
|
|
205
|
+
|
|
206
|
+
for (const targetColumn of targetColumns) {
|
|
207
|
+
const sourceColumn = sourceColumns.find(
|
|
208
|
+
(col) => col.column_name === targetColumn.column_name
|
|
209
|
+
);
|
|
543
210
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
`;
|
|
211
|
+
if (!sourceColumn) {
|
|
212
|
+
transformedRecord[targetColumn.column_name] = null;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
549
215
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
await this.
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
recordId ?? "(unknown)"
|
|
558
|
-
}' for ${targetSchema}.${tableName}: ${error.message}`
|
|
216
|
+
const value = record[targetColumn.column_name];
|
|
217
|
+
transformedRecord[targetColumn.column_name] =
|
|
218
|
+
await this.dataTransformer.transformColumnValue(
|
|
219
|
+
value,
|
|
220
|
+
targetColumn.column_name,
|
|
221
|
+
{ ...targetColumn, source_type: sourceColumn.data_type },
|
|
222
|
+
tenantId
|
|
559
223
|
);
|
|
560
|
-
this.logger.error(`Record data: ${JSON.stringify(record)}`);
|
|
561
|
-
// Consider logging transformedData and processedData as well for deep debugging
|
|
562
|
-
// this.logger.error(`Transformed data: ${JSON.stringify(transformedData)}`);
|
|
563
|
-
// this.logger.error(`Processed data (SQL values): ${JSON.stringify(processedData)}`);
|
|
564
|
-
// console.error("Underlying Error Stack:", error); // Log the original error stack
|
|
565
|
-
throw error; // Re-throw to stop the batch or be handled by the caller
|
|
566
|
-
}
|
|
567
224
|
}
|
|
225
|
+
|
|
226
|
+
return transformedRecord;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private async insertRecord(
|
|
230
|
+
client: any,
|
|
231
|
+
schema: string,
|
|
232
|
+
table: string,
|
|
233
|
+
record: any
|
|
234
|
+
): Promise<void> {
|
|
235
|
+
const columns = Object.keys(record);
|
|
236
|
+
const values = Object.values(record);
|
|
237
|
+
const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
|
|
238
|
+
|
|
239
|
+
const query = `
|
|
240
|
+
INSERT INTO "${schema}"."${table}"
|
|
241
|
+
(${columns.map((col) => `"${col}"`).join(", ")})
|
|
242
|
+
VALUES (${placeholders})
|
|
243
|
+
ON CONFLICT DO NOTHING
|
|
244
|
+
`;
|
|
245
|
+
|
|
246
|
+
await client.query(query, values);
|
|
568
247
|
}
|
|
569
248
|
}
|