UNPKG

autosql

Version:

An auto-parser of JSON into SQL.

706 lines (705 loc) 30.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.normalizeKeysArray = void 0; exports.isObject = isObject; exports.shuffleArray = shuffleArray; exports.validateConfig = validateConfig; exports.calculateColumnLength = calculateColumnLength; exports.normalizeNumber = normalizeNumber; exports.mergeColumnLengths = mergeColumnLengths; exports.setToArray = setToArray; exports.parseDatabaseLength = parseDatabaseLength; exports.parseDatabaseMetaData = parseDatabaseMetaData; exports.generateCombinations = generateCombinations; exports.isCombinationUnique = isCombinationUnique; exports.tableChangesExist = tableChangesExist; exports.isMetaDataHeader = isMetaDataHeader; exports.estimateRowSize = estimateRowSize; exports.isValidDataFormat = isValidDataFormat; exports.organizeSplitTable = organizeSplitTable; exports.organizeSplitData = organizeSplitData; exports.splitInsertData = splitInsertData; exports.getInsertValues = getInsertValues; exports.sqlize = sqlize; exports.getNextTableName = getNextTableName; exports.getTempTableName = getTempTableName; exports.getTrueTableName = getTrueTableName; exports.getHistoryTableName = getHistoryTableName; exports.wait_x_mseconds = wait_x_mseconds; exports.generateSafeConstraintName = generateSafeConstraintName; exports.normalizeResultKeys = normalizeResultKeys; exports.throwIfFailedResults = throwIfFailedResults; exports.normalizeName = normalizeName; const defaults_1 = require("../config/defaults"); const groupings_1 = require("../config/groupings"); const crypto_1 = __importDefault(require("crypto")); function isObject(val) { return val !== null && typeof val === "object"; } function shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array; } function validateConfig(config) { try { if (!config.sqlDialect) { throw new Error("Please provide a sqlDialect (such as pgsql, mysql) as part of the configuration object."); } // Define default values const defaultConfig = { sqlDialect: config.sqlDialect, // Keep required field pseudoUnique: defaults_1.defaults.pseudoUnique, autoIndexing: defaults_1.defaults.autoIndexing, sampling: defaults_1.defaults.sampling, samplingMinimum: defaults_1.defaults.samplingMinimum, metaData: config.metaData || {}, // Ensuring headers remain intact maxKeyLength: defaults_1.defaults.maxKeyLength, autoSplit: defaults_1.defaults.autoSplit, useWorkers: defaults_1.defaults.useWorkers, maxWorkers: defaults_1.defaults.maxWorkers, useStagingInsert: defaults_1.defaults.useStagingInsert, addHistory: defaults_1.defaults.addHistory, addTimestamps: defaults_1.defaults.addTimestamps, decimalMaxLength: defaults_1.defaults.decimalMaxLength, addNested: defaults_1.defaults.addNested, excludeBlankColumns: defaults_1.defaults.excludeBlankColumns, }; // Merge provided config with defaults return { ...defaultConfig, ...config }; } catch (error) { throw error; } } function calculateColumnLength(column, dataPoint, sqlLookupTable) { if (sqlLookupTable.decimals.includes(column.type)) { column.decimal = column.decimal ?? 0; const decimalLen = dataPoint.includes(".") ? dataPoint.split(".")[1].length + 1 : 0; column.decimal = Math.max(column.decimal, decimalLen); column.decimal = Math.min(column.decimal, sqlLookupTable.decimals_max_length || 10); const integerLen = dataPoint.split(".")[0].length; column.length = Math.max(column.length, integerLen + column.decimal + 3); } else { column.length = Math.max(column.length, dataPoint.length); } } function normalizeNumber(input, thousandsIndicatorOverride, decimalIndicatorOverride) { if ((thousandsIndicatorOverride && !decimalIndicatorOverride) || (!thousandsIndicatorOverride && decimalIndicatorOverride)) { throw new Error("Both 'thousandsIndicatorOverride' and 'decimalIndicatorOverride' must be provided together."); } let inputStr = String(input); let overridden = false; if (thousandsIndicatorOverride && decimalIndicatorOverride) { const THOUSANDS_INDICATORS = [",", "#*#*", "%*%*"]; const DECIMAL_INDICATORS = [".", "%*%*", "#*#*"]; const usedThousands = thousandsIndicatorOverride; const usedDecimal = decimalIndicatorOverride; const unusedThousands = THOUSANDS_INDICATORS.filter(ind => ind !== usedThousands && ind !== usedDecimal)[0]; const unusedDecimal = DECIMAL_INDICATORS.filter(ind => ind !== usedThousands && ind !== usedDecimal)[0]; overridden = true; // Temporarily replace thousands and decimal indicators with placeholders let tempinputStr = inputStr.replaceAll(usedThousands, unusedThousands); tempinputStr = tempinputStr.replaceAll(usedDecimal, unusedDecimal); // Replace placeholders with final characters (comma for thousands, dot for decimal) tempinputStr = tempinputStr.replaceAll(unusedThousands, ",").replaceAll(unusedDecimal, "."); inputStr = tempinputStr; } // 🚨 Ensure `-` appears only at the start if (inputStr.includes("-") && inputStr.indexOf("-") !== 0) return null; const isNegative = inputStr.startsWith("-"); if (isNegative) inputStr = inputStr.slice(1); // Remove `-` temporarily for processing if (!inputStr || /[^0-9., `']/.test(inputStr)) return null; // Reject if non-numeric characters exist. Allowing ` and ' as part of the Swiss number format const dotCount = (inputStr.match(/\./g) || []).length; let commaCount = (inputStr.match(/,/g) || []).length; // 🔍 Detect and normalize Swiss format if no commas are present but apostrophes exist if (commaCount === 0 && inputStr.includes("'")) { inputStr = inputStr.replace(/'/g, ","); // ✅ Convert apostrophes to commas commaCount = (inputStr.match(/,/g) || []).length; } if (commaCount === 0 && inputStr.includes("`")) { inputStr = inputStr.replace(/`/g, ","); commaCount = (inputStr.match(/,/g) || []).length; } inputStr = inputStr.replace(/ /g, ""); // 🚨 Reject cases if (!/\d/.test(inputStr) || // No digits present (dotCount > 1 && commaCount > 1) || // Too many of both inputStr.includes(".,") || inputStr.includes(",.") || // Misplaced combinations /\d[.,]{2,}\d/.test(inputStr) // Double separators like "1..234" ) { return null; } // 🚨 Check incorrect ordering of separators const firstComma = inputStr.indexOf(","); const lastComma = inputStr.lastIndexOf(","); const firstDot = inputStr.indexOf("."); const lastDot = inputStr.lastIndexOf("."); if (firstComma !== -1 && firstDot !== -1 && // Both exist ((firstComma < firstDot && dotCount > 1) || // Comma first, but multiple dots (firstDot < firstComma && commaCount > 1) || // Dot first, but multiple commas (firstComma < firstDot && firstDot < lastComma) || // Comma first, but comma after first dot (firstDot < firstComma && firstComma < lastDot) // Dot first, but dot after first comma )) { return null; } // Determine thousands and decimal indicators let thousandsIndicator = ""; let decimalIndicator = ""; if (overridden) { thousandsIndicator = ","; decimalIndicator = "."; } else if (dotCount === 1 && commaCount === 1) { thousandsIndicator = firstComma < firstDot ? "," : "."; decimalIndicator = thousandsIndicator === "," ? "." : ","; } else if (dotCount > 1) { thousandsIndicator = "."; decimalIndicator = ","; } else if (commaCount > 1) { thousandsIndicator = ","; decimalIndicator = "."; } else { // Only one separator exists, assume it is the decimal separator thousandsIndicator = ""; decimalIndicator = dotCount === 1 ? "." : ","; } const decimalSplit = inputStr.split(decimalIndicator); if (decimalSplit.length > 2) return null; // More than one decimal, invalid let preDecimal = decimalSplit[0]; let postDecimal = decimalSplit[1] || ""; // Optional decimal part // Validate thousands separator formatting if (thousandsIndicator) { const thousandsSplit = preDecimal.split(thousandsIndicator); if (thousandsSplit.length == 1) { const part = thousandsSplit[0]; if (part.length > 3) { return null; } } else { // 🔍 Detect if the format is Indian-style or Western-style const isWesternFormat = thousandsSplit.length > 1 && thousandsSplit.every((part, i) => (i === 0 ? part.length <= 3 : part.length === 3)); const isIndianFormat = thousandsSplit.length > 1 && thousandsSplit.every((part, i) => (i === 0 ? part.length <= 2 : i === thousandsSplit.length - 1 ? part.length === 3 : part.length === 2)); if (!isWesternFormat && !isIndianFormat) return null; // ❌ Reject if it fits neither format // ✅ If valid, remove thousands separators } preDecimal = thousandsSplit.join(""); } const normalized = `${isNegative ? "-" : ""}${preDecimal}${postDecimal ? "." + postDecimal : ""}`; return normalized; } function mergeColumnLengths(lengthA, lengthB) { if (!lengthA && !lengthB) return undefined; const parseLength = (length) => { const parts = length.split(",").map(Number); return parts.length === 2 ? parts : [parts[0], 0]; // Ensure decimal part exists }; const [lenA, decA] = lengthA ? parseLength(lengthA) : [0, 0]; const [lenB, decB] = lengthB ? parseLength(lengthB) : [0, 0]; return `${Math.max(lenA, lenB)},${Math.max(decA, decB)}`; } function setToArray(inputSet) { return [...inputSet]; // Spread operator converts Set to an array } function parseDatabaseLength(lengthStr) { if (!lengthStr) return {}; const parts = lengthStr.split(",").map(Number); const length = isNaN(parts[0]) ? undefined : parts[0]; const decimal = parts.length === 2 && !isNaN(parts[1]) ? parts[1] : undefined; return { length, decimal }; } function parseDatabaseMetaData(rows, dialectConfig) { if (!rows || rows.length === 0) return null; // Return null if no data const hasTableName = rows.some(row => "table_name" in row || "TABLE_NAME" in row); const hasNoTableName = rows.some(row => !("table_name" in row) && !("TABLE_NAME" in row)); if (hasTableName && hasNoTableName) { throw new Error("Inconsistent data: Some rows contain 'table_name' while others do not."); } const metadata = {}; rows.forEach((row) => { const normalizedRow = Object.keys(row).reduce((acc, key) => { acc[key.toLowerCase()] = row[key]; return acc; }, {}); if (!normalizedRow.column_name) return; // Skip invalid rows const lengthInfo = parseDatabaseLength(String(normalizedRow["length"])); const dataType = dialectConfig?.translate?.serverToLocal[normalizedRow.data_type.toLowerCase()] || normalizedRow["data_type"].toLowerCase(); const columnKey = (normalizedRow["column_key"] || "").toUpperCase(); let normalizedLength = lengthInfo.length; if (dialectConfig?.noLength.includes(dataType)) { normalizedLength = undefined; } else if (dialectConfig?.optionalLength.includes(dataType) && lengthInfo.length === undefined) { normalizedLength = undefined; } const autoIncrement = String(normalizedRow["extra"] || "").includes("auto_increment") || String(normalizedRow["column_default"] || "").includes("nextval"); const tableName = normalizedRow.table_name || "noTableName"; // Default for single-table case if (!metadata[tableName]) { metadata[tableName] = {}; } metadata[tableName][normalizedRow.column_name] = { type: dataType, length: normalizedLength, allowNull: normalizedRow["is_nullable"] === "YES", unique: columnKey === "UNIQUE", primary: columnKey === "PRIMARY", index: columnKey === "INDEX", autoIncrement: autoIncrement, decimal: lengthInfo.decimal ?? undefined, default: normalizedRow["column_default"], }; }); return hasTableName ? metadata : metadata["noTableName"] || null; } function generateCombinations(array, length) { if (length === 1) return array.map(el => [el]); const combinations = []; for (let i = 0; i < array.length; i++) { const smallerCombinations = generateCombinations(array.slice(i + 1), length - 1); for (const smaller of smallerCombinations) { combinations.push([array[i], ...smaller]); } } return combinations; } function isCombinationUnique(data, columns) { const seenValues = new Set(); for (const row of data) { const key = columns.map(col => row[col]).join("|"); if (seenValues.has(key)) return false; seenValues.add(key); } return true; } function tableChangesExist(alterTableChanges) { if (Object.keys(alterTableChanges.addColumns).length > 0 || Object.keys(alterTableChanges.modifyColumns).length > 0 || alterTableChanges.dropColumns.length > 0 || alterTableChanges.renameColumns.length > 0 || alterTableChanges.nullableColumns.length > 0 || alterTableChanges.noLongerUnique.length > 0 || alterTableChanges.primaryKeyChanges.length > 0) { return true; } else { return false; } } function isMetaDataHeader(input) { if (typeof input !== "object" || input === null || Array.isArray(input)) { return false; // ❌ Must be a non-null object } for (const key in input) { if (typeof key !== "string") return false; // ❌ Keys must be strings const column = input[key]; if (typeof column !== "object" || column === null || (!("type" in column) || typeof column.type !== "string") // ✅ "type" is required and must be a string ) { return false; } // ✅ Optional fields must match expected types if (("length" in column && column.length != null && typeof column.length !== "number") || ("allowNull" in column && column.allowNull != null && typeof column.allowNull !== "boolean") || ("unique" in column && column.unique != null && typeof column.unique !== "boolean") || ("index" in column && column.index != null && typeof column.index !== "boolean") || ("pseudounique" in column && column.pseudounique != null && typeof column.pseudounique !== "boolean") || ("primary" in column && column.primary != null && typeof column.primary !== "boolean") || ("autoIncrement" in column && column.autoIncrement != null && typeof column.autoIncrement !== "boolean") || ("decimal" in column && column.decimal != null && typeof column.decimal !== "number")) { return false; } } return true; // ✅ Passed all checks } function estimateRowSize(mergedMetaData, dbType) { let totalSize = 0; for (const columnName in mergedMetaData) { const column = mergedMetaData[columnName]; const type = column.type?.toLowerCase() || "varchar"; let columnSize = 0; if (["boolean", "binary", "tinyint"].includes(type)) { columnSize = 1; } else if (["smallint"].includes(type)) { columnSize = 2; } else if (["int", "numeric"].includes(type)) { columnSize = 4; } else if (["bigint"].includes(type)) { columnSize = 8; } else if (["decimal", "double", "exponent"].includes(type)) { columnSize = column.decimal ? Math.ceil(column.decimal / 2) + 1 : defaults_1.DEFAULT_LENGTHS.decimal; } else if (["varchar"].includes(type)) { columnSize = column.length ?? defaults_1.DEFAULT_LENGTHS.varchar; } else if (["text", "mediumtext", "longtext", "json"].includes(type)) { columnSize = defaults_1.DEFAULT_LENGTHS[type] ?? 4; // Only store pointer size } else if (["date"].includes(type)) { columnSize = 3; } else if (["time"].includes(type)) { columnSize = 3; } else if (["datetime", "datetimetz"].includes(type)) { columnSize = 8; } if (column.allowNull) { columnSize += 1; // Add 1 byte for NULL flag } if (column.primary || column.unique || column.index) { columnSize += 8; // Approximate index storage } totalSize += columnSize; } // Add row overhead (~20 bytes for metadata, depends on storage engine) const rowOverhead = 20; totalSize += rowOverhead; let maxRowSize; if (dbType === 'mysql') { maxRowSize = defaults_1.MYSQL_MAX_ROW_SIZE; } else if (dbType === 'pgsql') { maxRowSize = defaults_1.POSTGRES_MAX_ROW_SIZE; } else { maxRowSize = defaults_1.POSTGRES_MAX_ROW_SIZE; } return { rowSize: totalSize, exceedsLimit: totalSize > maxRowSize, nearlyExceedsLimit: totalSize > maxRowSize * 0.8 }; } function isValidDataFormat(data) { return Array.isArray(data) && data.length > 0 && typeof data[0] === "object" && data[0] !== null && !Array.isArray(data[0]); } const normalizeKeysArray = (data) => { return data.map(obj => Object.keys(obj).reduce((acc, key) => { acc[key.toLowerCase()] = obj[key]; return acc; }, {})); }; exports.normalizeKeysArray = normalizeKeysArray; function organizeSplitTable(table, newMetaData, currentMetaData, dialectConfig) { let normalizedMetaData; // ✅ Check if currentMetaData is already in structured format if (typeof currentMetaData === "object" && !Array.isArray(currentMetaData)) { if (Object.values(currentMetaData).some(value => typeof value === "object" && !Array.isArray(value))) { // ✅ Already `Record<string, MetadataHeader>`, use it directly normalizedMetaData = currentMetaData; } else { // ✅ If it's `MetadataHeader`, wrap it in `{ table: MetadataHeader }` normalizedMetaData = { [table]: currentMetaData }; } } else { // ✅ Otherwise, assume it's raw DB results and parse const parsedMetadata = parseDatabaseMetaData(currentMetaData, dialectConfig); if (!parsedMetadata) { normalizedMetaData = { [table]: {} }; // ✅ Ensure it has a valid structure } else if (Object.values(parsedMetadata).some(value => typeof value === "object" && !Array.isArray(value))) { normalizedMetaData = parsedMetadata; // ✅ Multiple tables } else { normalizedMetaData = { [table]: parsedMetadata }; // ✅ Single table } } const primaryKeys = {}; const newColumns = {}; const allTablesEmpty = Object.values(normalizedMetaData).every(table => Object.keys(table).length === 0); const newGroupedByTable = Object.entries(newMetaData).reduce((acc, [columnName, columnDef]) => { if (allTablesEmpty) { if (columnDef.primary) { primaryKeys[columnName] = columnDef; } else { newColumns[columnName] = columnDef; } return acc; } const matchingTables = Object.keys(normalizedMetaData).filter(table => Object.prototype.hasOwnProperty.call(normalizedMetaData[table], columnName)); if (matchingTables.length > 0) { matchingTables.forEach(tableName => { if (!acc[tableName]) acc[tableName] = {}; acc[tableName][columnName] = columnDef; }); if (columnDef.primary) { primaryKeys[columnName] = columnDef; } } else { newColumns[columnName] = columnDef; } return acc; }, {}); let tableName = Object.keys(newGroupedByTable).pop() || getNextTableName(Object.keys(newGroupedByTable).pop() || table); const unallocatedColumns = { ...newColumns }; while (Object.keys(unallocatedColumns).length > 0) { // ✅ Check the row size before adding new columns for (var i = 0; i < Object.keys(unallocatedColumns).length; i++) { const currentTableData = newGroupedByTable[tableName] || { ...primaryKeys }; const columnName = Object.keys(unallocatedColumns)[i]; const columnDef = unallocatedColumns[columnName]; const mergedMetaData = { ...currentTableData, [columnName]: columnDef }; // Simulate adding column const columnCount = Object.keys(mergedMetaData).length; const exceedsColumnLimit = columnCount >= defaults_1.MAX_COLUMN_COUNT; const { exceedsLimit, nearlyExceedsLimit } = estimateRowSize(mergedMetaData, dialectConfig.dialect); if (!nearlyExceedsLimit && !exceedsColumnLimit) { // ✅ Add the column if within limits if (!newGroupedByTable[tableName]) { newGroupedByTable[tableName] = { ...primaryKeys }; // Ensure primary keys exist in new table } newGroupedByTable[tableName][columnName] = columnDef; delete unallocatedColumns[columnName]; // ✅ Remove from unallocated list i--; } else { tableName = getNextTableName(tableName); i--; } } } return newGroupedByTable; } function organizeSplitData(data, splitMetaData) { const groupedData = {}; data.forEach((row) => { // ✅ Initialize an object for each table's row data const rowDataByTable = {}; Object.entries(splitMetaData).forEach(([tableName, columns]) => { rowDataByTable[tableName] = {}; // ✅ Ensure each table has a row initialized Object.keys(columns).forEach((columnName) => { if (row.hasOwnProperty(columnName)) { rowDataByTable[tableName][columnName] = row[columnName]; } }); // ✅ Only add to groupedData if it has at least one column if (Object.keys(rowDataByTable[tableName]).length > 0) { if (!groupedData[tableName]) { groupedData[tableName] = []; } groupedData[tableName].push(rowDataByTable[tableName]); } }); }); return groupedData; } function splitInsertData(data, config) { const { insertStack = 1000 } = config; const chunks = []; for (let i = 0; i < data.length; i += insertStack) { chunks.push(data.slice(i, i + insertStack)); } return chunks; } function getInsertValues(metaData, row, dialectConfig, databaseConfig, sqlizeValues = false) { const newRow = Object.entries(metaData).map(([column, meta]) => { let value = row[column]; if (value === null || value === undefined) { // Use calculated default if provided if (meta.calculatedDefault !== undefined) { value = meta.calculatedDefault; } else if (meta.default !== undefined) { value = meta.default; } else { value = null; } } if (sqlizeValues && dialectConfig) { const sqlizedValue = sqlize(value, meta.type, dialectConfig, databaseConfig); return sqlizedValue; } else { return value; } }); return newRow; } function sqlize(value, columnType, dialectConfig, databaseConfig) { try { if (value === null) return null; if (!columnType) { return value; } ; const type = columnType.toLowerCase(); const rules = dialectConfig.sqlize; if (type === "json") { try { if (typeof value === "string") { try { // Try parsing it first (in case it's a JSON string) const parsed = JSON.parse(value); return JSON.stringify(parsed); // ✅ Store re-stringified version } catch { // ❌ Failed to parse: just return original string return value; } } else if (typeof value === "object") { // ✅ Valid object → stringify return JSON.stringify(value); } else { // ⚠️ Unexpected type (number, boolean, etc.) return JSON.stringify({ value }); } } catch (err) { console.warn(`[sqlize] Failed to handle JSON value for column:`, { value, error: err.message || err }); return null; // ❌ Fallback to NULL if completely unusable } } let strValue = typeof value === "string" ? value : String(value); const isDateLike = groupings_1.groupings.dateGroup.includes(columnType); if (isDateLike) { const match = strValue.match(/\/Date\((\d+)(?:[+-]\d+)?\)\//); if (match) { const millis = parseInt(match[1], 10); strValue = new Date(millis).toISOString(); } else { const cleaned = strValue.replace(/[^\d:-\sT]/g, ""); const parsedDate = new Date(cleaned); if (!isNaN(parsedDate.getTime())) { strValue = parsedDate.toISOString(); } } } const isNumberLike = groupings_1.groupings.intGroup.includes(columnType) || groupings_1.groupings.specialIntGroup.includes(columnType); if (isNumberLike) { const normalised = normalizeNumber(value) || strValue; const precision = databaseConfig?.decimalMaxLength ?? defaults_1.defaults.decimalMaxLength; strValue = roundStringDecimal(normalised, precision); } for (const rule of rules) { const appliesToType = rule.type === true || (Array.isArray(rule.type) && rule.type.includes(type)); if (appliesToType) { const regex = new RegExp(rule.regex, "g"); strValue = strValue.replace(regex, rule.replace); } } if (strValue === '' || strValue === 'null') { return null; } return strValue; } catch (error) { return value; } } function getNextTableName(tableName) { const match = tableName.match(/^(.*?)(__part_(\d+))?$/); // Match `table__part_001` if (match && match[3]) { const baseName = match[1]; // Extract "table" const num = parseInt(match[3], 10) + 1; // Increment existing number return `${baseName}__part_${String(num).padStart(3, "0")}`; // Zero-padded } return `${tableName}__part_001`; // If no number exists, start at __part_001 } ; function getTempTableName(tableName) { const TEMP_PREFIX = "temp_staging__"; return tableName.startsWith(TEMP_PREFIX) ? tableName : `${TEMP_PREFIX}${tableName}`; } function getTrueTableName(tableName) { const TEMP_PREFIX = "temp_staging__"; const HISTORY_SUFFIX = "__history"; let result = tableName; if (result.startsWith(TEMP_PREFIX)) { result = result.slice(TEMP_PREFIX.length); } if (result.endsWith(HISTORY_SUFFIX)) { result = result.slice(0, -HISTORY_SUFFIX.length); } return result; } function getHistoryTableName(tableName) { const HISTORY_SUFFIX = "__history"; return tableName.endsWith(HISTORY_SUFFIX) ? tableName : `${tableName}${HISTORY_SUFFIX}`; } async function wait_x_mseconds(x) { return new Promise(resolve => { setTimeout(() => { resolve(null); }, x); }); } function roundStringDecimal(valueStr, precision) { if (!valueStr.includes('.')) return valueStr; const [intPart, decimalPartRaw] = valueStr.split('.'); const decimalPart = decimalPartRaw.slice(0, precision); const nextDigit = decimalPartRaw.charAt(precision); if (!nextDigit || parseInt(nextDigit, 10) < 5) { // No rounding needed, just trim excess return decimalPart.length > 0 ? `${intPart}.${decimalPart}` : intPart; } // Perform manual rounding let full = `${intPart}.${decimalPart}`; let roundedNum = Number(full); const multiplier = Math.pow(10, precision); roundedNum = Math.round(roundedNum * multiplier) / multiplier; return roundedNum.toString(); } function generateSafeConstraintName(table, column, type = 'unique') { const base = `${table}_${column}_${type}`; if (base.length <= 63) return base; // Truncate and append a hash for uniqueness const hash = crypto_1.default.createHash('md5').update(base).digest('hex').slice(0, 6); const truncated = base.slice(0, 63 - hash.length - 1); // -1 for underscore return `${truncated}_${hash}`; } function normalizeResultKeys(row) { return Object.fromEntries(Object.entries(row).map(([key, value]) => [key.toLowerCase(), value])); } function throwIfFailedResults(results, action = "operation") { const failed = results.filter(r => !r.success); if (failed.length > 0) { const message = `One or more ${action} failed (${failed.length}):\n` + failed .map(r => `- ${r.table || "Unknown Table"}: ${r.error || "Unknown Error"}`) .join("\n"); throw new Error(message); } } function normalizeName(name) { return name.toLowerCase().replace(/[^a-z0-9]/g, ""); }