@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.
Files changed (58) hide show
  1. package/dist/scripts/data-migration/batch-migrator.d.ts +14 -19
  2. package/dist/scripts/data-migration/batch-migrator.js +98 -297
  3. package/dist/scripts/data-migration/batch-migrator.js.map +1 -1
  4. package/dist/scripts/data-migration/data-transformer.d.ts +16 -7
  5. package/dist/scripts/data-migration/data-transformer.js +169 -133
  6. package/dist/scripts/data-migration/data-transformer.js.map +1 -1
  7. package/dist/scripts/data-migration/db-connector.d.ts +6 -1
  8. package/dist/scripts/data-migration/db-connector.js +44 -8
  9. package/dist/scripts/data-migration/db-connector.js.map +1 -1
  10. package/dist/scripts/data-migration/dependency-resolver.d.ts +10 -10
  11. package/dist/scripts/data-migration/dependency-resolver.js +92 -211
  12. package/dist/scripts/data-migration/dependency-resolver.js.map +1 -1
  13. package/dist/scripts/data-migration/foreign-key-manager.d.ts +6 -5
  14. package/dist/scripts/data-migration/foreign-key-manager.js +108 -18
  15. package/dist/scripts/data-migration/foreign-key-manager.js.map +1 -1
  16. package/dist/scripts/data-migration/migration-config.json +63 -0
  17. package/dist/scripts/data-migration/migration-tool.d.ts +25 -6
  18. package/dist/scripts/data-migration/migration-tool.js +78 -38
  19. package/dist/scripts/data-migration/migration-tool.js.map +1 -1
  20. package/dist/scripts/data-migration/multi-source-migrator.d.ts +17 -0
  21. package/dist/scripts/data-migration/multi-source-migrator.js +130 -0
  22. package/dist/scripts/data-migration/multi-source-migrator.js.map +1 -0
  23. package/dist/scripts/data-migration/schema-utils.d.ts +3 -3
  24. package/dist/scripts/data-migration/schema-utils.js +62 -19
  25. package/dist/scripts/data-migration/schema-utils.js.map +1 -1
  26. package/dist/scripts/data-migration/tenant-migrator.js +9 -2
  27. package/dist/scripts/data-migration/tenant-migrator.js.map +1 -1
  28. package/dist/scripts/data-migration/typecast-manager.d.ts +7 -3
  29. package/dist/scripts/data-migration/typecast-manager.js +169 -25
  30. package/dist/scripts/data-migration/typecast-manager.js.map +1 -1
  31. package/dist/scripts/data-migration/types.d.ts +68 -2
  32. package/dist/scripts/fix-table-indexes.d.ts +26 -0
  33. package/dist/scripts/fix-table-indexes.js +460 -0
  34. package/dist/scripts/fix-table-indexes.js.map +1 -0
  35. package/dist/scripts/multi-db-migration.d.ts +1 -0
  36. package/dist/scripts/multi-db-migration.js +55 -0
  37. package/dist/scripts/multi-db-migration.js.map +1 -0
  38. package/dist/scripts/run-migration.js +41 -75
  39. package/dist/scripts/run-migration.js.map +1 -1
  40. package/dist/tsconfig.tsbuildinfo +1 -1
  41. package/migration-config.json +40 -72
  42. package/{migration-config-public.json → migration-config.json.bk} +14 -14
  43. package/package.json +6 -3
  44. package/src/scripts/data-migration/batch-migrator.ts +192 -513
  45. package/src/scripts/data-migration/data-transformer.ts +252 -203
  46. package/src/scripts/data-migration/db-connector.ts +66 -13
  47. package/src/scripts/data-migration/dependency-resolver.ts +121 -266
  48. package/src/scripts/data-migration/foreign-key-manager.ts +214 -32
  49. package/src/scripts/data-migration/migration-config.json +63 -0
  50. package/src/scripts/data-migration/migration-tool.ts +377 -225
  51. package/src/scripts/data-migration/schema-utils.ts +94 -32
  52. package/src/scripts/data-migration/tenant-migrator.ts +12 -5
  53. package/src/scripts/data-migration/typecast-manager.ts +186 -31
  54. package/src/scripts/data-migration/types.ts +78 -5
  55. package/src/scripts/dumps/source_dump_20250428_145606.sql +323 -0
  56. package/src/scripts/fix-table-indexes.ts +602 -0
  57. package/src/scripts/post-migration-validator.ts +206 -107
  58. 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 { DatabaseConnections } from "./types";
7
- import { DependencyResolver } from "./dependency-resolver";
8
- import { ForeignKeyManager } from "./foreign-key-manager"; // Assuming this exists and might be useful later
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 BATCH_SIZE = 10; // Consider making this configurable
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 dependencyResolver: DependencyResolver, // Keep for dependency checks
20
- private readonly schemaCache: Record<string, ColumnSchema[]> = {},
21
- private readonly targetSchemaCache: Record<string, ColumnSchema[]> = {} // Removed ForeignKeyManager from constructor if not used directly here
22
- ) {}
24
+ private readonly options: MigrationOptions,
25
+ private readonly providerId?: string | null
26
+ ) {
27
+ this.typecastManager = new TypecastManager();
28
+ }
23
29
 
24
- private async checkTableHasData(
25
- tenantId: string,
26
- tableName: string
27
- ): Promise<boolean> {
28
- // Keep this function as is
29
- try {
30
- const result = await this.connections.targetPool.query(
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
- async migrateEntityDataInBatches(
43
- prisma: PrismaClient, // Prisma client (likely connected to target public schema)
44
- entity: EntityType, // Config object for the table being migrated
45
- providerId: number | null, // Provider ID to filter source data (null if not filtering)
46
- targetSchema: string // The schema in the TARGET database where data should be inserted
47
- ) {
48
- try {
49
- // Ensure target schema exists (important for tenant schemas if used)
50
- if (targetSchema !== "public") {
51
- await this.ensureSchemaExists(targetSchema);
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
- // Optional: Dependency check (can be complex with dynamic filtering)
55
- // Consider if this check is still reliable or needed with the new strategy
56
- // const dependencies = await this.dependencyResolver.analyzeDependencies();
57
- // ... dependency check logic ...
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
- // Disable foreign key checks in the target schema for the duration of this batch
60
- // Note: This applies to the connection used by targetPool, ensure it targets the correct DB if replicas are used
61
- await this.connections.targetPool.query(
62
- `SET session_replication_role = 'replica';`
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
- const { name: tableName, idField, filterColumn, filterVia } = entity;
80
+ await this.validateSchema(sourceColumns, targetColumns);
66
81
 
67
- this.logger.log(
68
- `Migrating ${tableName} -> target schema '${targetSchema}'. ${
69
- providerId
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
- // Get source and target schemas
77
- const sourceTableSchema = await this.getSourceSchema(tableName);
78
- const targetTableSchema = await this.getTargetSchema(
79
- targetSchema,
80
- tableName
88
+ const records = await this.fetchBatch(
89
+ sourceSchema,
90
+ tableConfig,
91
+ offset,
92
+ BATCH_SIZE
81
93
  );
82
94
 
83
- // Validate schemas
84
- if (!sourceTableSchema.length) {
85
- this.logger.warn(
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
- selectQuery = `SELECT t.* ${fromClause} ${whereClause}`;
142
- // --- END DYNAMIC SOURCE QUERY GENERATION ---
143
-
144
- // Execute query and process data
145
- const sourceData = await this.executeSourceQuery(
146
- selectQuery,
147
- queryParams
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
- if (totalRecords === 0) {
156
- this.logger.log(
157
- `No records to migrate for ${tableName} with current filter. Skipping processing.`
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
- // Re-enable FK checks before returning
160
- await this.connections.targetPool.query(
161
- `SET session_replication_role = 'origin';`
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
- // --- Helper Functions (ensureSchemaExists, getSourceSchema, getTargetSchema, executeSourceQuery, getPrimaryKeyField) ---
211
- // Keep these mostly as they are, but ensure getPrimaryKeyField uses the correct idField from EntityType if needed
212
- // (Current implementation queries information_schema, which is fine, but ensure it uses the correct targetSchema)
213
-
214
- private async ensureSchemaExists(schemaName: string): Promise<void> {
215
- // Check if schema exists
216
- const schemaExistsResult = await this.connections.targetPool.query(
217
- `SELECT schema_name FROM information_schema.schemata WHERE schema_name = $1`,
218
- [schemaName]
219
- );
220
-
221
- if (schemaExistsResult.rows.length === 0) {
222
- this.logger.log(`Schema '${schemaName}' does not exist. Creating...`);
223
- // Create schema if it doesn't exist - Needs structure copied from public
224
- await this.schemaUtils.createSchema(schemaName);
225
- this.logger.log(`Schema '${schemaName}' created.`);
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 async getSourceSchema(tableName: string): Promise<ColumnSchema[]> {
247
- const cacheKey = `source.${tableName}`;
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
- private async getTargetSchema(
260
- schema: string, // Target schema name (could be 'public' or tenantId)
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
- private async executeSourceQuery(query: string, params: any[]): Promise<any> {
276
- try {
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
- private async getPrimaryKeyField(
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
- // --- processRecords ---
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
- sourceSchema: ColumnSchema[],
333
- targetSchemaInfo: ColumnSchema[] // Renamed to avoid confusion
163
+ targetSchema: string,
164
+ tableConfig: TableConfig,
165
+ sourceColumns: ColumnSchema[],
166
+ targetColumns: ColumnSchema[],
167
+ tenantId: string
334
168
  ): Promise<void> {
335
- if (!idField) {
336
- this.logger.error(
337
- `Cannot process records for ${tableName}: idField is missing in configuration.`
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
- // Transform data using the target schema *info* for type checking etc.
365
- const transformedData = await this.dataTransformer.transformRecord(
173
+ for (const record of records) {
174
+ const transformedRecord = await this.transformRecord(
366
175
  record,
367
- sourceSchema,
368
- targetSchemaInfo, // Use the schema structure info
369
- targetSchema // Pass target schema for enum validation context etc.
176
+ sourceColumns,
177
+ targetColumns,
178
+ tenantId
370
179
  );
371
180
 
372
- // Prepare data for raw query, ensuring correct types and casting strings
373
- const processedData = Object.entries(transformedData).reduce(
374
- (acc, [key, value]) => {
375
- const columnSchema = targetSchemaInfo.find(
376
- (col) => col.column_name === key
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
- const columns = Object.keys(validProcessedData);
520
- const valuesString = Object.values(validProcessedData).join(", "); // Values are already SQL strings
521
-
522
- if (columns.length === 0) {
523
- this.logger.warn(
524
- `Record ${recordId} for ${tableName} resulted in no valid columns to insert/update after processing. Skipping.`
525
- );
526
- continue;
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
- // Only include DO UPDATE clause if there are columns to update
540
- const conflictClause = updateSetClauses
541
- ? `ON CONFLICT (${quotedIdField}) DO UPDATE SET ${updateSetClauses}`
542
- : `ON CONFLICT (${quotedIdField}) DO NOTHING`;
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
- const query = `
545
- INSERT INTO ${quotedSchemaTable} (${columns.join(", ")})
546
- VALUES (${valuesString})
547
- ${conflictClause}
548
- `;
211
+ if (!sourceColumn) {
212
+ transformedRecord[targetColumn.column_name] = null;
213
+ continue;
214
+ }
549
215
 
550
- // Execute using targetPool connection for raw SQL flexibility
551
- this.logger.debug(`Executing Upsert: ${query.replace(/\s\s+/g, " ")}`);
552
- await this.connections.targetPool.query(query);
553
- } catch (error) {
554
- // Improved error logging
555
- this.logger.error(
556
- `Error processing record ID '${
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
  }