UNPKG

autosql

Version:

An auto-parser of JSON into SQL.

698 lines (697 loc) 34.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AutoSQLHandler = void 0; const metadata_1 = require("../helpers/metadata"); const utilities_1 = require("../helpers/utilities"); const defaults_1 = require("../config/defaults"); const timestamps_1 = require("../helpers/timestamps"); const workerHelper_1 = __importDefault(require("../workers/workerHelper")); class AutoSQLHandler { db; constructor(dbInstance) { this.db = dbInstance; } async autoCreateTable(table, newMetaData, tableExists, runQuery = true) { try { // ✅ Skip table existence check if already known if (tableExists === undefined) { const checkTableExistsQuery = this.db.getTableExistsQuery(this.db.getConfig().schema || this.db.getConfig().database || "", table); const checkTableExists = await this.db.runQuery(checkTableExistsQuery); if (!checkTableExists.success || !checkTableExists.results) { throw new Error(`Failed to check schema existence: ${checkTableExists.error}`); } tableExists = Boolean(Number(checkTableExists?.results[0]?.count)); } if (tableExists) { throw new Error("Table already exists"); } // ✅ Create the table const createQuery = this.db.getCreateTableQuery(table, newMetaData); if (!runQuery) { return createQuery; } const createTable = await this.db.runTransaction(createQuery); return createTable; } catch (error) { throw error; } } async autoAlterTable(table, tableChanges, tableExists, runQuery = true) { try { // ✅ Skip table existence check if already known if (tableExists === undefined) { const checkTableExistsQuery = this.db.getTableExistsQuery(this.db.getConfig().schema || this.db.getConfig().database || "", table); const checkTableExists = await this.db.runQuery(checkTableExistsQuery); if (!checkTableExists.success || !checkTableExists.results) { throw new Error(`Failed to check schema existence: ${checkTableExists.error}`); } tableExists = Boolean(Number(checkTableExists?.results[0]?.count)); } if (!tableExists) { throw new Error("Table doesn't exist"); } // ✅ Alter the table const alterQuery = await this.db.getAlterTableQuery(table, tableChanges); if (!runQuery) { return alterQuery; } const alterTable = await this.db.runTransaction(alterQuery); return alterTable; } catch (error) { throw error; } } async autoConfigureTable(inputOrTable, inputData, inputCurrentMetaData, inputNewMetaData, inputRunQuery = true) { try { let table; let data = null; let currentMetaDataOrTableChanges = null; let newMetaData = null; let runQuery = true; if (typeof inputOrTable === "object") { table = inputOrTable.table; data = inputOrTable.data; currentMetaDataOrTableChanges = inputOrTable.previousMetaData; newMetaData = inputOrTable.metaData; runQuery = inputOrTable.runQuery || inputRunQuery || true; } else { // ✅ Handle case where `input` is a `string` (table name) table = inputOrTable; data = inputData ?? null; currentMetaDataOrTableChanges = inputCurrentMetaData ?? null; newMetaData = inputNewMetaData ?? null; runQuery = inputRunQuery; } console.log(`⚡ [autoConfigureTable] Running for table: ${table}`); if (!newMetaData && data?.length === 0) { // ❌ Cannot configure table '${table}': No existing metadata and no data provided to infer structure. throw new Error(`No existing metadata and no data provided to infer structure.`); } let tableChanges = null; let updatedMetadata = newMetaData; let tableExists = undefined; if (!newMetaData) { if (!data || !(0, utilities_1.isValidDataFormat)(data)) { throw new Error('Invalid data format: Expected a non-empty array of objects.'); } newMetaData = await (0, metadata_1.getMetaData)(this.db.getConfig(), data); updatedMetadata = newMetaData; this.db.updateTableMetadata(table, newMetaData, "metaData"); } if (!updatedMetadata) { throw new Error('An unexpected error occurred while getting metadata'); } if (!currentMetaDataOrTableChanges) { console.log("🔎 Fetching metadata since no current metadata was provided..."); const { currentMetaData, tableExists: exists } = await this.fetchTableMetadata(table); tableExists = exists; if (currentMetaData) { // ✅ Compare metadata if table exists const { changes, updatedMetaData: mergedMetadata } = (0, metadata_1.compareMetaData)(currentMetaData, updatedMetadata, this.db.getDialectConfig()); tableChanges = changes; updatedMetadata = mergedMetadata; this.db.updateTableMetadata(table, updatedMetadata, "metaData"); } } else if ((0, utilities_1.isMetaDataHeader)(currentMetaDataOrTableChanges)) { console.log("🔍 Comparing metadata for changes..."); // ✅ If provided with metadata, compare changes const { changes, updatedMetaData: mergedMetadata } = (0, metadata_1.compareMetaData)(currentMetaDataOrTableChanges, newMetaData, this.db.getDialectConfig()); tableChanges = changes; updatedMetadata = mergedMetadata; tableExists = true; } else { console.log("🚀 Precomputed table changes detected, using them directly."); // ✅ If provided with precomputed table changes, use directly tableChanges = currentMetaDataOrTableChanges; updatedMetadata = newMetaData; // ✅ No merging needed tableExists = true; } if (!tableExists) { console.log(`🔨 Creating table: ${table}`); return await this.autoCreateTable(table, updatedMetadata, false, runQuery); } // ✅ If table exists but no changes, return success if (!tableChanges || !(0, utilities_1.tableChangesExist)(tableChanges)) { console.log(`✅ Table exists, no changes detected. Skipping ALTER TABLE.`); const start = this.db.startDate; const end = new Date(); const affectedRows = 0; const rows = []; return { start, end, duration: end.getTime() - start.getTime(), affectedRows, success: true, results: rows }; } // ✅ If table exists and changes exist, alter it console.log(`✏️ Altering table: ${table} with changes:`, tableChanges); return await this.autoAlterTable(table, tableChanges, true, runQuery); } catch (error) { const start = this.db.startDate; const end = new Date(); const affectedRows = 0; return { start, end, duration: end.getTime() - start.getTime(), affectedRows, success: false, error: error instanceof Error ? error.message : String(error), table: typeof inputOrTable === "object" ? inputOrTable.table : inputOrTable }; } } async fetchTableMetadata(table) { // ✅ Check if the table exists const checkTableExistsQuery = this.db.getTableExistsQuery(this.db.getConfig().schema || this.db.getConfig().database || "", table); const checkTableExists = await this.db.runQuery(checkTableExistsQuery); const tableExists = Boolean(Number(checkTableExists?.results?.[0]?.count || 0)); let currentMetaData = null; if (tableExists) { // ✅ Fetch metadata ONLY if table exists const currentMetaDataQuery = this.db.getTableMetaDataQuery(this.db.getConfig().schema || this.db.getConfig().database || "", table); const currentMetaDataResults = await this.db.runQuery(currentMetaDataQuery); if (!currentMetaDataResults || !currentMetaDataResults.success || !currentMetaDataResults.results) { throw new Error(`Failed to retrieve existing meta data for table ${table}`); } const parsedMetadata = (0, utilities_1.parseDatabaseMetaData)(currentMetaDataResults.results, this.db.getDialectConfig()); if (!parsedMetadata) { currentMetaData = null; } else if (typeof parsedMetadata === "object" && !Array.isArray(parsedMetadata)) { // ✅ Ensure that we only get MetadataHeader, not multiple tables currentMetaData = parsedMetadata; } else { throw new Error("Unexpected metadata format: Multiple tables returned for a single-table query."); } if (currentMetaData) { this.db.updateTableMetadata(table, currentMetaData, "existingMetaData"); } } return { currentMetaData, tableExists }; } async splitTableData(table, data, metaData) { try { const splitQuery = this.db.getSplitTablesQuery(table); const currentSplitResults = await this.db.runQuery(splitQuery); if (!currentSplitResults || !currentSplitResults.success || !currentSplitResults.results) { throw new Error(currentSplitResults.error || `Error while retrieving existing split table information for: ${table}`); } const currentSplit = currentSplitResults.results; let parsedSplitMetadata = (0, utilities_1.parseDatabaseMetaData)(currentSplit, this.db.getDialectConfig()); if (!parsedSplitMetadata) { parsedSplitMetadata = { [table]: {} }; // ✅ Ensure it has a valid structure } else if (Object.values(parsedSplitMetadata).some(value => typeof value === "object" && !Array.isArray(value))) { parsedSplitMetadata = parsedSplitMetadata; } else { parsedSplitMetadata = { [table]: parsedSplitMetadata }; } const newGroupedByTable = (0, utilities_1.organizeSplitTable)(table, metaData, parsedSplitMetadata, this.db.getDialectConfig()); const newGroupedData = (0, utilities_1.organizeSplitData)(data, newGroupedByTable); const transformedData = await Promise.all(Object.keys(newGroupedByTable).map(async (tableName) => { const newMetaData = await (0, metadata_1.getMetaData)(this.db.getConfig(), newGroupedData[tableName] || []); const mergedMetaData = (0, metadata_1.compareMetaData)(parsedSplitMetadata[tableName], newMetaData, this.db.getDialectConfig()); return { table: tableName, data: newGroupedData[tableName] || [], metaData: newMetaData, previousMetaData: parsedSplitMetadata[tableName], mergedMetaData: mergedMetaData }; })); return transformedData; } catch (error) { throw error; } } async autoInsertData(inputOrTable, inputData, inputMetaData, inputPreviousMetaData, inputComparedMetaData, inputRunQuery = true, inputInsertType) { let table; let data = []; let metaData; let previousMetaData = null; let comparedMetaData; let runQuery; let insertType; // ✅ Support InsertInput object if (typeof inputOrTable === "object" && "table" in inputOrTable && "data" in inputOrTable) { table = inputOrTable.table; data = inputOrTable.data; metaData = inputOrTable.metaData; previousMetaData = inputOrTable.previousMetaData; comparedMetaData = inputOrTable.comparedMetaData; runQuery = inputOrTable.runQuery ?? true; insertType = inputOrTable?.insertType || "UPDATE"; } else { // ✅ Support individual parameters table = inputOrTable; data = inputData ?? []; metaData = inputMetaData; previousMetaData = inputPreviousMetaData ?? null; comparedMetaData = inputComparedMetaData; runQuery = inputRunQuery ?? true; insertType = inputInsertType || "UPDATE"; } if (data.length === 0) { throw new Error(`insertData: no data rows provided for table "${table}"`); } const splitData = (0, utilities_1.splitInsertData)(data, this.db.getConfig()); const effectiveMetaData = comparedMetaData?.updatedMetaData || metaData; const insertStatements = await Promise.all(splitData.map((chunk) => { const normalisedChunk = chunk.map((row) => (0, utilities_1.getInsertValues)(effectiveMetaData, row, this.db.getDialectConfig(), this.db.getConfig(), true)); return this.db.getInsertStatementQuery(table, normalisedChunk, effectiveMetaData, insertType); })); if (insertStatements.length > 0 && runQuery) { return await this.db.runTransaction(insertStatements); } return insertStatements; } async handleMetadata(table, data, primaryKey) { // Fetch existing metadata const { currentMetaData } = await this.fetchTableMetadata(table); // Generate new metadata from incoming data const newMetaData = await (0, metadata_1.getMetaData)(this.db.getConfig(), data, primaryKey); this.db.updateTableMetadata(table, newMetaData, "metaData"); let initialComparedMetaData; let mergedMetaData = newMetaData; let changes = null; if (currentMetaData) { initialComparedMetaData = (0, metadata_1.compareMetaData)(currentMetaData, newMetaData, this.db.getDialectConfig()); changes = initialComparedMetaData.changes; mergedMetaData = initialComparedMetaData.updatedMetaData; this.db.updateTableMetadata(table, mergedMetaData, "metaData"); } mergedMetaData = (0, timestamps_1.ensureTimestamps)(this.db.getConfig(), mergedMetaData, this.db.startDate); return { currentMetaData, mergedMetaData, initialComparedMetaData, changes, newMetaData }; } async attemptTableSplit(table, data, mergedMetaData) { if (this.db.getConfig().autoSplit) { const { rowSize, exceedsLimit } = (0, utilities_1.estimateRowSize)(mergedMetaData, this.db.getDialect()); const columnCount = Object.keys(mergedMetaData).length; const exceedsColumnLimit = columnCount >= defaults_1.MAX_COLUMN_COUNT; if (exceedsLimit || exceedsColumnLimit) { return await this.splitTableData(table, data, mergedMetaData); } } return []; } async prepareInsertData(table, data, schema, primaryKey) { // 🔹 Step 1: Handle Metadata const { currentMetaData, mergedMetaData, initialComparedMetaData, changes, newMetaData } = await this.handleMetadata(table, data, primaryKey); // 🔹 Step 2: Attempt Table Split let insertInput = await this.attemptTableSplit(table, data, mergedMetaData); // 🔹 Step 3: Handle the case when split is not needed or failed if (!insertInput || insertInput.length === 0) { // 🔹 Step 3.1: Handle metadata comparison if not split let comparedMetaData = initialComparedMetaData; if (comparedMetaData === undefined) { comparedMetaData = (0, metadata_1.compareMetaData)(currentMetaData || null, newMetaData, this.db.getDialectConfig()); } insertInput = [{ table, data, previousMetaData: changes || currentMetaData, metaData: mergedMetaData, comparedMetaData }]; } return insertInput; } async configureTables(insertInput) { if (this.db.getConfig().safeMode) return []; let configuredTables; // 🔹 Step 1: Auto-configure tables (with Workers or Directly) if (this.db.getConfig().useWorkers) { insertInput = insertInput.map((input) => ({ ...input, runQuery: false })); const workerResults = await workerHelper_1.default.run(this.db.getConfig(), "autoConfigureTable", insertInput); const failed = workerResults.filter(w => !w.success); if (failed.length > 0) { throw new Error(`Worker execution failed for ${failed.length} task(s):\n` + failed.map((f, i) => `- Task #${i + 1}: ${f?.error?.message || "Unknown Error"}`).join("\n")); } configuredTables = workerResults.map(w => w.result); } else { configuredTables = await Promise.all(insertInput.map((input) => this.autoConfigureTable(input))); } // 🔹 Step 2: Split successful results and queries to execute const initialResults = configuredTables.filter((result) => "success" in result); const queryInputs = configuredTables.filter((result) => Array.isArray(result)); // 🔹 Step 3: Execute Transactions (if needed) let allResults; if (queryInputs.length > 0) { const transactionResults = await this.db.runTransactionsWithConcurrency(queryInputs); allResults = [...initialResults, ...transactionResults]; } else { allResults = [...initialResults]; } // 🔹 Step 4: Handle failures (0, utilities_1.throwIfFailedResults)(allResults, "table configuring queries"); console.log("✅ All tables configured and executed successfully."); return allResults; } async insertData(insertInput) { if (insertInput.length === 0) { throw new Error("No data found for insert after tables were configured"); } let insertQueries; // 🔹 Step 1: Handle single insert separately if (insertInput.length === 1) { insertQueries = [ await this.autoInsertData({ ...insertInput[0], runQuery: false }) ]; } else { // 🔹 Step 2: Defer execution & modify inputs insertInput = insertInput.map((input) => ({ ...input, runQuery: false })); if (this.db.getConfig().useWorkers) { const workerResults = await workerHelper_1.default.run(this.db.getConfig(), "autoInsertData", insertInput); const failed = workerResults.filter(w => !w.success); if (failed.length > 0) { throw new Error(`Worker execution failed for ${failed.length} task(s):\n` + failed.map((f, i) => `- Task #${i + 1}: ${f?.error?.message || "Unknown Error"}`).join("\n")); } insertQueries = workerResults.map(w => w.result); } else { insertQueries = await Promise.all(insertInput.map((input) => this.autoInsertData({ ...input, runQuery: false }))); } } // 🔹 Step 3: Execute Insert Transactions const insertTransactionInputs = insertQueries; const allInsertResults = await this.db.runTransactionsWithConcurrency(insertTransactionInputs); // 🔹 Step 4: Handle Failures (0, utilities_1.throwIfFailedResults)(allInsertResults, "data insert queries"); return allInsertResults; } async prepareStagingTables(insertInput) { const uniqueTables = Array.from(new Set(insertInput.map(input => input.table))); const stagingQueries = uniqueTables.map(table => { return [this.db.getCreateTempTableQuery(table)]; }); const allCreateResults = await this.db.runTransactionsWithConcurrency(stagingQueries); (0, utilities_1.throwIfFailedResults)(allCreateResults, "table create queries"); return allCreateResults; } async insertStagingTables(insertInput) { const stagingInputs = insertInput.map(input => ({ ...input, table: (0, utilities_1.getTempTableName)(input.table), insertType: "INSERT" })); // Configure staging tables where necessary await this.configureTables(stagingInputs); return await this.insertData(stagingInputs); } async removeStagingTables(insertInput) { const uniqueTables = Array.from(new Set(insertInput.map(input => input.table))); const stagingQueries = uniqueTables.map(table => { const tempTableName = (0, utilities_1.getTempTableName)(table); return [this.db.getDropTableQuery(tempTableName)]; }); const allDropResults = await this.db.runTransactionsWithConcurrency(stagingQueries); (0, utilities_1.throwIfFailedResults)(allDropResults, "table drop queries"); return allDropResults; } async resolveConflicts(insertInput) { const uniqueTables = Array.from(new Set(insertInput.map(input => input.table))); const uniqueIndexesQuery = uniqueTables.map(table => { return [this.db.getUniqueIndexesQuery(table)]; }); const primaryKeyQuery = uniqueTables.map(table => { return [this.db.getPrimaryKeysQuery(table)]; }); const allUniqueKeys = await this.db.runTransactionsWithConcurrency(uniqueIndexesQuery); const allPrimaryKeys = await this.db.runTransactionsWithConcurrency(primaryKeyQuery); const tableStructure = {}; for (let i = 0; i < uniqueTables.length; i++) { const table = uniqueTables[i]; const uniqueIndexes = allUniqueKeys[i]; const primaryColumns = allPrimaryKeys[i]; if (!uniqueIndexes?.results) continue; if (!primaryColumns?.results) continue; const normalizedUniques = uniqueIndexes.results .map(row => (0, utilities_1.normalizeResultKeys)(row)) .filter(row => row.columns); const normalizedPrimary = primaryColumns.results .map(row => (0, utilities_1.normalizeResultKeys)(row)) .filter(row => row.column_name); if (!tableStructure[table]) { tableStructure[table] = { uniques: {}, primary: [] }; } normalizedUniques.forEach(result => { tableStructure[table].uniques[result.index_name] = result.columns .split(",") .map(col => col.trim()); }); normalizedPrimary.forEach(result => { tableStructure[table].primary.push(result.column_name); }); } const conflictsQuery = Object.keys(tableStructure) .filter(table => { const structure = tableStructure[table]; return (structure && Object.keys(structure.uniques || {}).length > 0 && // Must have at least one unique constraint Array.isArray(structure.primary) && structure.primary.length > 0 // Must have at least one primary key ); }) .map(table => { return [this.db.getConstraintConflictQuery(table, tableStructure[table])]; }); const allConflicts = await this.db.runTransactionsWithConcurrency(conflictsQuery); const constraintViolations = {}; let removeConstraintsQuery = []; for (let i = 0; i < allConflicts.length; i++) { const result = allConflicts[i]; const table = Object.keys(tableStructure)[i]; let tableConstraintsQueries = []; const row = result?.results?.[0] || {}; const violatingIndexes = []; for (const [indexName, count] of Object.entries(row)) { const numericCount = typeof count === "string" ? parseInt(count) : Number(count); if (numericCount > 0) { violatingIndexes.push(indexName); tableConstraintsQueries.push(this.db.getDropUniqueConstraintQuery(table, indexName)); } } if (violatingIndexes.length) { constraintViolations[table] = violatingIndexes; removeConstraintsQuery.push(tableConstraintsQueries); } } const removeConstraints = await this.db.runTransactionsWithConcurrency(removeConstraintsQuery); (0, utilities_1.throwIfFailedResults)(removeConstraints, 'unique constraint removal queries'); return; } async insertFromStagingTables(insertInput) { const stagingInputs = insertInput.map(input => ({ ...input, insertType: "UPDATE" })); const stagingInsertQueries = (stagingInputs).map(stagingInput => { return [this.db.getInsertFromStagingQuery(stagingInput)]; }); const allInsertResults = await this.db.runTransactionsWithConcurrency(stagingInsertQueries); (0, utilities_1.throwIfFailedResults)(allInsertResults, 'insert from staging table queries'); return allInsertResults; } async insertToHistoryTables(insertInputs) { const stagingInsertQueries = (insertInputs).map(insertInput => { return [this.db.getInsertChangedRowsToHistoryQuery(insertInput)]; }); const allInsertResults = await this.db.runTransactionsWithConcurrency(stagingInsertQueries); (0, utilities_1.throwIfFailedResults)(allInsertResults, 'insert from staging table queries'); return allInsertResults; } async insertHistory(insertInput) { const config = this.db.getConfig(); if (!config.addHistory || !config.historyTables?.length) return []; if (!this.db.getConfig().useStagingInsert) { throw new Error('Cannot add history tables without using staging insert'); } const uniqueTables = Array.from(new Set(insertInput.map(input => input.table))); const eligibleInputs = uniqueTables.filter(table => config.historyTables.includes(table)); if (eligibleInputs.length == 0) { return []; } // Check for current table meta data const historyInputs = await Promise.all(eligibleInputs.map(async (table) => { const historyName = (0, utilities_1.getHistoryTableName)(table); // Run both metadata fetches in parallel const [currentStatus, historyStatus] = await Promise.all([ this.fetchTableMetadata(table), this.fetchTableMetadata(historyName), ]); const currentMetaData = currentStatus.currentMetaData; const currentHistoryMetaData = historyStatus.currentMetaData; if (!currentMetaData) { throw new Error(`Could not find structure of ${table} for history table creation`); } // ✅ Clean up metaData for history table const cleanedMeta = {}; for (const col in currentMetaData) { const def = { ...currentMetaData[col] }; def.unique = false; def.index = false; cleanedMeta[col] = def; } // ✅ Add as_at column cleanedMeta["dwh_as_at"] = { type: "datetime", allowNull: false, primary: true, }; const automatedColumns = ['dwh_created_at', 'dwh_modified_at', 'dwh_loaded_at']; // ✅ Ensure existing PKs are retained for (const col in currentMetaData) { if (currentMetaData[col].primary) { cleanedMeta[col].primary = true; } if (automatedColumns.includes(col)) { cleanedMeta[col].calculated = true; } } return { table: historyName, data: [], metaData: cleanedMeta, previousMetaData: currentHistoryMetaData, }; })); const configuredHistory = await this.configureTables(historyInputs); const insertedHistory = await this.insertToHistoryTables(historyInputs); return insertedHistory; } async extractNestedInputs(inputs) { if (!this.db.getConfig().addNested) { return []; } const nestedInputs = []; const nestedMap = {}; const primaryMap = {}; for (const input of inputs) { const { table, data, metaData } = input; const primaryKeys = Object.keys(metaData).filter(k => metaData[k].primary); for (const row of data) { for (const [key, value] of Object.entries(row)) { const nestedTable = `${table}_${key}`; // Check if the key is a nested table if (!this.db.getConfig().nestedTables?.includes(nestedTable)) { continue; } if (value && typeof value === "object") { const nestedObjects = Array.isArray(value) ? value.filter(v => typeof v === "object" && !Array.isArray(v)) : [value]; for (const nested of nestedObjects) { const newRow = { ...nested, ...Object.fromEntries(primaryKeys.map(pk => [pk, row[pk]])) }; // Group by nested table name if (!nestedMap[nestedTable]) { nestedMap[nestedTable] = []; } if (!primaryMap[nestedTable]) { primaryMap[nestedTable] = primaryKeys; } nestedMap[nestedTable].push(newRow); } } } } } for (const [nestedTable, nestedRows] of Object.entries(nestedMap)) { const { currentMetaData, mergedMetaData, initialComparedMetaData, changes, newMetaData } = await this.handleMetadata(nestedTable, nestedRows, primaryMap[nestedTable]); let comparedMetaData = initialComparedMetaData; if (comparedMetaData === undefined) { comparedMetaData = (0, metadata_1.compareMetaData)(currentMetaData || null, newMetaData, this.db.getDialectConfig()); } const insertInput = { table: nestedTable, data: nestedRows, previousMetaData: changes || currentMetaData, metaData: mergedMetaData, comparedMetaData }; nestedInputs.push(insertInput); } return nestedInputs; } async autoSQL(table, data, schema, primaryKey) { try { if (schema) { this.db.updateSchema(schema); } let affectedRows; let insertResults; let insertInput = await this.prepareInsertData(table, data, schema, primaryKey); let nestedInputs = await this.extractNestedInputs(insertInput); insertInput = [...insertInput, ...nestedInputs]; await this.configureTables(insertInput); if (this.db.getConfig().useStagingInsert) { await this.prepareStagingTables(insertInput); await this.insertStagingTables(insertInput); await this.resolveConflicts(insertInput); await this.insertHistory(insertInput); insertResults = await this.insertFromStagingTables(insertInput); await this.removeStagingTables(insertInput); } else { insertResults = await this.insertData(insertInput); } insertResults = []; const start = this.db.startDate; affectedRows = insertResults.reduce((sum, res) => sum + (res.affectedRows || 0), 0); const allResults = insertResults.flatMap(res => res.results || []); const end = new Date; return { start, end, success: true, duration: end.getTime() - start.getTime(), affectedRows, results: allResults, table }; } catch (error) { const start = this.db.startDate; const end = new Date(); const affectedRows = 0; return { start, end, duration: end.getTime() - start.getTime(), affectedRows, success: false, error: error instanceof Error ? error.message : String(error) // Ensure error is a string }; } } } exports.AutoSQLHandler = AutoSQLHandler;