Created
October 1, 2024 14:33
-
-
Save MarkusJLechner/2fadfd2a4e318b738ef235cbd9f18de1 to your computer and use it in GitHub Desktop.
Convert mysql dump to sqlite3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| const fs = require('fs'); | |
| const readline = require('readline'); | |
| // Function to print to stderr | |
| function printerr(s) { | |
| process.stderr.write(s + '\n'); | |
| } | |
| // Initialize INT_MAX_HALF based on JavaScript's safe integer limits | |
| const INT_MAX_HALF = Math.floor(Number.MAX_SAFE_INTEGER / 2); | |
| // Function to convert bit fields to integers | |
| function bit_to_int(str_bit) { | |
| let powtwo = 1; | |
| let overflow = 0; | |
| let res = 0; | |
| for (let i = str_bit.length - 1; i >= 0; i--) { | |
| const bit = str_bit.charAt(i); | |
| if (overflow || (bit === '1' && res > INT_MAX_HALF)) { | |
| printerr( | |
| `${lineNumber}: WARN Bit field overflow, number truncated (LSBs saved, MSBs ignored).` | |
| ); | |
| break; | |
| } | |
| res += bit * powtwo; | |
| if (powtwo > INT_MAX_HALF) { | |
| overflow = 1; | |
| continue; | |
| } | |
| powtwo *= 2; | |
| } | |
| return res; | |
| } | |
| // Check command-line arguments | |
| if (process.argv.length !== 3) { | |
| printerr( | |
| 'USAGE:\n' + | |
| ' mysql2sqlite dump_mysql.sql > dump_sqlite3.sql\n' + | |
| ' OR\n' + | |
| ' mysql2sqlite dump_mysql.sql | sqlite3 sqlite.db\n' + | |
| '\n' + | |
| 'NOTES:\n' + | |
| ' Dash in filename is not supported, because dash (-) means stdin.' | |
| ); | |
| process.exit(1); | |
| } | |
| const inputFile = process.argv[2]; | |
| // Initialize variables | |
| let inTrigger = 0; | |
| let inView = 0; | |
| let prev = ''; | |
| let firstInTable = true; | |
| let tableName = ''; | |
| let key = {}; // Stores index statements | |
| let caseIssue = 0; | |
| let lineNumber = 0; | |
| let currentTable = null; | |
| let tableDefinitions = {}; // To store CREATE TABLE definitions | |
| let tableColumns = {}; // Stores list of columns for each table | |
| // New variables to handle ALTER TABLE statements | |
| let inAlterTable = false; | |
| let alterTableLines = []; | |
| // New variable to track if a primary key has been defined | |
| let primaryKeyDefined = {}; | |
| // Read the input file | |
| const rl = readline.createInterface({ | |
| input: fs.createReadStream(inputFile), | |
| crlfDelay: Infinity, | |
| }); | |
| // Output initial SQLite pragmas | |
| console.log('PRAGMA synchronous = OFF;'); | |
| console.log('PRAGMA journal_mode = MEMORY;'); | |
| console.log('BEGIN TRANSACTION;'); | |
| rl.on('line', (line) => { | |
| lineNumber++; | |
| // Handle CREATE TRIGGER statements | |
| if (/^\/\*.*(CREATE.*TRIGGER|create.*trigger)/.test(line)) { | |
| line = line.replace(/^.*(TRIGGER|trigger)/, 'CREATE TRIGGER'); | |
| console.log(line); | |
| inTrigger = 1; | |
| return; | |
| } | |
| // Handle end of CREATE TRIGGER | |
| if (/(END|end) \*\/;;/.test(line)) { | |
| line = line.replace(/\*\//, ''); | |
| console.log(line); | |
| inTrigger = 0; | |
| return; | |
| } | |
| // Print lines inside a trigger | |
| if (inTrigger !== 0) { | |
| console.log(line); | |
| return; | |
| } | |
| // Handle CREATE VIEW statements | |
| if (/^\/\*.*(CREATE.*TABLE|create.*table)/.test(line)) { | |
| inView = 1; | |
| return; | |
| } | |
| if (/^(\).*(ENGINE|engine).*\*\/;)/.test(line)) { | |
| inView = 0; | |
| return; | |
| } | |
| if (inView !== 0) { | |
| return; | |
| } | |
| // Skip comments | |
| if (/^\/\*/.test(line)) { | |
| return; | |
| } | |
| // Skip PARTITION statements | |
| if (/^ *[(]?(PARTITION|partition) +[^ ]+/.test(line)) { | |
| return; | |
| } | |
| // Handle INSERT statements | |
| if ( | |
| (/^ *\(/.test(line) && /\) *[,;] *$/.test(line)) || | |
| /^(INSERT|insert|REPLACE|replace)/.test(line) | |
| ) { | |
| prev = ''; | |
| // Handle escaped characters | |
| line = line.replace(/\\\\/g, '\\_'); | |
| line = line.replace(/\\'/g, "''"); | |
| line = line.replace(/\\n/g, '\n'); | |
| line = line.replace(/\\r/g, '\r'); | |
| line = line.replace(/\\"/g, '"'); | |
| line = line.replace(/\\\032/g, '\032'); | |
| line = line.replace(/\\_/g, '\\'); | |
| // Trim long hex numbers | |
| let hexIssue = false; | |
| while (/0x[0-9a-fA-F]{17}/.test(line)) { | |
| hexIssue = true; | |
| line = line.replace(/0x[0-9a-fA-F]+/, (match) => | |
| match.substr(0, match.length - 1) | |
| ); | |
| } | |
| if (hexIssue) { | |
| printerr( | |
| `${lineNumber}: WARN Hex number trimmed (length longer than 16 chars).` | |
| ); | |
| hexIssue = false; | |
| } | |
| console.log(line); | |
| return; | |
| } | |
| // Skip CREATE DATABASE statements | |
| if (/^(CREATE DATABASE|create database)/.test(line)) { | |
| return; | |
| } | |
| // Handle CREATE TABLE statements | |
| if (/^(CREATE|create)/.test(line)) { | |
| if ( | |
| /IF NOT EXISTS|if not exists/.test(line) || | |
| /TEMPORARY|temporary/.test(line) | |
| ) { | |
| caseIssue = 1; | |
| printerr( | |
| `${lineNumber}: WARN Potential case sensitivity issues with table/column naming\n` + | |
| ' (see INFO at the end).' | |
| ); | |
| } | |
| // Updated regex to capture table names more robustly | |
| const tableNameMatch = line.match( | |
| /^CREATE TABLE\s+(IF NOT EXISTS\s+)?[`"]?([^`" ]+)[`"]?/i | |
| ); | |
| if (tableNameMatch) { | |
| tableName = tableNameMatch[2]; | |
| currentTable = tableName.replace(/[`"]/g, '').toLowerCase(); | |
| tableDefinitions[currentTable] = []; // Initialize an array to store table lines | |
| tableColumns[currentTable] = new Set(); // Initialize set to store column names | |
| primaryKeyDefined[currentTable] = false; // Initialize primary key tracker | |
| } | |
| prev = ''; | |
| firstInTable = true; | |
| tableDefinitions[currentTable].push(line); | |
| return; | |
| } | |
| // Replace FULLTEXT KEY statements | |
| if (/^ (FULLTEXT KEY|fulltext key)/.test(line)) { | |
| line = line.replace(/[A-Za-z ]+(KEY|key)/, ' KEY'); | |
| } | |
| // Remove field lengths in KEY lines | |
| if (/ (PRIMARY |primary )?(KEY|key)/.test(line)) { | |
| line = line.replace(/\([0-9]+\)/g, ''); | |
| } | |
| // Skip duplicate PRIMARY KEY definitions | |
| if (/PRIMARY KEY|primary key/.test(line)) { | |
| if (primaryKeyDefined[currentTable]) { | |
| // Already have a primary key, skip this one | |
| return; | |
| } else { | |
| primaryKeyDefined[currentTable] = true; | |
| } | |
| } | |
| // Replace COLLATE statements | |
| if (/ (COLLATE|collate) [a-z0-9_]*/.test(line)) { | |
| line = line.replace(/(COLLATE|collate) [a-z0-9_]*/g, 'COLLATE BINARY'); | |
| } | |
| // Handle field definitions | |
| if ( | |
| currentTable && | |
| /^ /.test(line) && | |
| !/^( (KEY|key)| (CONSTRAINT|constraint)|\);)/.test(line) | |
| ) { | |
| // Parse the column definition | |
| const columnDefMatch = line.match(/^\s*[`"]([^`"]+)[`"]\s+([^\s]+)(.*)$/); | |
| if (columnDefMatch) { | |
| const columnName = columnDefMatch[1]; | |
| let dataType = columnDefMatch[2]; | |
| let restOfLine = columnDefMatch[3]; | |
| // Add column name to tableColumns | |
| if (!tableColumns[currentTable]) { | |
| tableColumns[currentTable] = new Set(); | |
| } | |
| tableColumns[currentTable].add(columnName); | |
| // Remove UNSIGNED | |
| dataType = dataType.replace(/UNSIGNED|unsigned/g, ''); | |
| // Replace integer types with INTEGER | |
| dataType = dataType.replace( | |
| /\b(TINYINT|SMALLINT|MEDIUMINT|INT|INTEGER|BIGINT)\b/gi, | |
| 'INTEGER' | |
| ); | |
| // If AUTO_INCREMENT is present in restOfLine | |
| if (/AUTO_INCREMENT|auto_increment/i.test(restOfLine)) { | |
| // Check if dataType is INTEGER | |
| if (dataType.trim().toUpperCase() === 'INTEGER') { | |
| if (!primaryKeyDefined[currentTable]) { | |
| // Replace AUTO_INCREMENT with PRIMARY KEY AUTOINCREMENT | |
| restOfLine = restOfLine.replace( | |
| /AUTO_INCREMENT|auto_increment/gi, | |
| 'PRIMARY KEY AUTOINCREMENT' | |
| ); | |
| primaryKeyDefined[currentTable] = true; | |
| } else { | |
| // Remove AUTO_INCREMENT, issue warning | |
| restOfLine = restOfLine.replace( | |
| /AUTO_INCREMENT|auto_increment/gi, | |
| '' | |
| ); | |
| printerr( | |
| `WARN: Multiple AUTO_INCREMENT columns in table ${currentTable}. Only one INTEGER PRIMARY KEY AUTOINCREMENT is allowed in SQLite.` | |
| ); | |
| } | |
| } else { | |
| // Remove AUTO_INCREMENT, issue warning | |
| restOfLine = restOfLine.replace( | |
| /AUTO_INCREMENT|auto_increment/gi, | |
| '' | |
| ); | |
| printerr( | |
| `WARN: AUTO_INCREMENT column ${columnName} in table ${currentTable} is not INTEGER type. Removed AUTO_INCREMENT.` | |
| ); | |
| } | |
| } | |
| // Reconstruct the line | |
| line = ` "${columnName}" ${dataType}${restOfLine}`; | |
| // Proceed with other replacements as before | |
| line = line.replace( | |
| /(UNIQUE KEY|unique key) (`.*`|".*") /g, | |
| 'UNIQUE ' | |
| ); | |
| line = line.replace( | |
| /(CHARACTER SET|character set) [^ ]+[ ,]/g, | |
| '' | |
| ); | |
| line = line.replace( | |
| /(ON|on) (UPDATE|update) (CURRENT_TIMESTAMP|current_timestamp)(\(\))?/g, | |
| '' | |
| ); | |
| line = line.replace( | |
| /(DEFAULT|default) (CURRENT_TIMESTAMP|current_timestamp)(\(\))?/g, | |
| 'DEFAULT current_timestamp' | |
| ); | |
| line = line.replace(/(COLLATE|collate) [^ ]+ /g, ''); | |
| // Corrected handling of ENUM and SET data types | |
| line = line.replace(/(ENUM|enum)\s*\([^)]+\)/g, 'text '); | |
| line = line.replace(/(SET|set)\s*\([^)]+\)/g, 'text '); | |
| // Handle bit fields | |
| const ere_bit_field = /[bB]'[10]+'/; | |
| const bitFieldMatch = line.match(ere_bit_field); | |
| if (bitFieldMatch) { | |
| const bitValue = bitFieldMatch[0].substring( | |
| 2, | |
| bitFieldMatch[0].length - 1 | |
| ); | |
| line = line.replace(ere_bit_field, bit_to_int(bitValue)); | |
| } | |
| // Remove USING BTREE and other suffixes | |
| line = line.replace(/ USING [^, ]+/g, ''); | |
| // Remove field comments | |
| line = line.replace(/ (COMMENT|comment).+$/, ''); | |
| // Remove commas at end | |
| line = line.replace(/,$/, ''); | |
| if (prev) { | |
| // Add previous line to table definitions | |
| if (firstInTable) { | |
| tableDefinitions[currentTable].push(prev); | |
| firstInTable = false; | |
| } else { | |
| tableDefinitions[currentTable].push(',' + prev); | |
| } | |
| } | |
| prev = line; | |
| return; | |
| } else { | |
| // If the line doesn't match, perhaps do nothing | |
| printerr( | |
| `WARN: Could not parse column definition in table ${currentTable}: ${line}` | |
| ); | |
| return; | |
| } | |
| } else { | |
| // Not within a column definition | |
| if (prev) { | |
| // Add previous line to table definitions | |
| if (firstInTable) { | |
| tableDefinitions[currentTable].push(prev); | |
| firstInTable = false; | |
| } else { | |
| tableDefinitions[currentTable].push(',' + prev); | |
| } | |
| prev = ''; | |
| } | |
| } | |
| // Handle KEY and CONSTRAINT definitions inside CREATE TABLE | |
| if ( | |
| currentTable && | |
| (/^( (KEY|key)| (UNIQUE KEY|unique key)| (CONSTRAINT|constraint))/.test(line) || | |
| line.trim() === ');') | |
| ) { | |
| // Add any remaining 'prev' content | |
| if (prev) { | |
| if (firstInTable) { | |
| tableDefinitions[currentTable].push(prev); | |
| firstInTable = false; | |
| } else { | |
| tableDefinitions[currentTable].push(',' + prev); | |
| } | |
| prev = ''; | |
| } | |
| if (line.trim() === ');') { | |
| if (currentTable && tableDefinitions[currentTable]) { | |
| tableDefinitions[currentTable].push(line); | |
| } | |
| } else { | |
| // Process KEY or CONSTRAINT lines | |
| if (/^( (KEY|key)| (UNIQUE KEY|unique key))/.test(line)) { | |
| // Process INDEX definitions | |
| const indexNameMatch = line.match(/KEY\s+[`"]?([^`"]*)[`"]?\s*\(([^)]+)\)/i); | |
| if (indexNameMatch) { | |
| let indexName = indexNameMatch[1] || 'auto_index'; | |
| let indexKey = indexNameMatch[2]; | |
| const unique = /UNIQUE KEY|unique key/.test(line) ? 'UNIQUE ' : ''; | |
| // Split indexKey into individual columns and clean them | |
| const indexColumns = indexKey | |
| .split(',') | |
| .map((col) => col.trim().replace(/[`"]/g, '')); | |
| // Check if all columns exist in the table | |
| let allColumnsExist = true; | |
| indexColumns.forEach((col) => { | |
| if (!tableColumns[currentTable] || !tableColumns[currentTable].has(col)) { | |
| allColumnsExist = false; | |
| printerr( | |
| `WARN: Column '${col}' in index '${indexName}' does not exist in table '${currentTable}'. Index will not be created.` | |
| ); | |
| } | |
| }); | |
| if (allColumnsExist) { | |
| const indexLine = | |
| 'CREATE ' + | |
| unique + | |
| 'INDEX "' + | |
| currentTable + | |
| '_' + | |
| indexName + | |
| '" ON "' + | |
| currentTable + | |
| '" (' + | |
| indexKey + | |
| ');'; | |
| if (!key[currentTable]) { | |
| key[currentTable] = ''; | |
| } | |
| key[currentTable] += indexLine + '\n'; | |
| } else { | |
| // Skip creating this index | |
| } | |
| } else { | |
| printerr(`WARN: Could not parse INDEX line: ${line}`); | |
| } | |
| } else if (/^( (CONSTRAINT|constraint))/.test(line)) { | |
| // Process FOREIGN KEY constraints | |
| const fkMatch = line.match( | |
| /CONSTRAINT [`"]?([^`"]*)[`"]? FOREIGN KEY\s*\(([^)]+)\)\s*REFERENCES\s+[`"]?([^`"]*)[`"]?\s*\(([^)]+)\)/i | |
| ); | |
| if (fkMatch) { | |
| const constraintName = fkMatch[1]; | |
| const localColumns = fkMatch[2]; | |
| const referencedTable = fkMatch[3]; | |
| const referencedColumns = fkMatch[4]; | |
| // Split local columns in case there are multiple | |
| const localColsArray = localColumns | |
| .split(',') | |
| .map((col) => col.trim().replace(/[`"]/g, '')); | |
| // Ensure all local columns exist in the table | |
| let allColumnsExist = true; | |
| localColsArray.forEach((columnName) => { | |
| if (!tableColumns[currentTable] || !tableColumns[currentTable].has(columnName)) { | |
| allColumnsExist = false; | |
| printerr( | |
| `WARN: Column '${columnName}' in FOREIGN KEY constraint does not exist in table '${currentTable}'. Constraint will not be created.` | |
| ); | |
| } | |
| }); | |
| if (allColumnsExist) { | |
| // Build FOREIGN KEY constraint | |
| const foreignKeyStatement = `FOREIGN KEY (${localColumns}) REFERENCES ${referencedTable}(${referencedColumns})`; | |
| // Add to table definitions | |
| if (firstInTable) { | |
| tableDefinitions[currentTable].push(foreignKeyStatement); | |
| firstInTable = false; | |
| } else { | |
| tableDefinitions[currentTable].push(',' + foreignKeyStatement); | |
| } | |
| } else { | |
| // Skip creating this constraint | |
| } | |
| } else { | |
| printerr(`WARN: Could not parse CONSTRAINT line: ${line}`); | |
| } | |
| } | |
| } | |
| return; | |
| } | |
| // Handle ENGINE statements (end of CREATE TABLE) | |
| if (/ ENGINE| engine/.test(line)) { | |
| // Add any remaining 'prev' content | |
| if (prev) { | |
| if (firstInTable) { | |
| tableDefinitions[currentTable].push(prev); | |
| firstInTable = false; | |
| } else { | |
| tableDefinitions[currentTable].push(',' + prev); | |
| } | |
| prev = ''; | |
| } | |
| // Add closing parenthesis | |
| if (currentTable && tableDefinitions[currentTable]) { | |
| tableDefinitions[currentTable].push(');'); | |
| } | |
| currentTable = null; | |
| firstInTable = true; | |
| return; | |
| } | |
| // Handle ALTER TABLE statements | |
| if (!inAlterTable) { | |
| if (/^ALTER TABLE/.test(line)) { | |
| inAlterTable = true; | |
| alterTableLines = [line]; | |
| return; | |
| } | |
| } else { | |
| alterTableLines.push(line); | |
| if (line.trim().endsWith(';')) { | |
| // We've collected the entire ALTER TABLE statement | |
| // Now process it | |
| const alterTableStatement = alterTableLines.join(' '); | |
| inAlterTable = false; | |
| // Process alterTableStatement | |
| const alterMatch = alterTableStatement.match(/^ALTER TABLE\s+[`"]?([^`"]+)[`"]?\s+(.+);$/i); | |
| if (alterMatch) { | |
| let alterTableName = alterMatch[1]; | |
| let alterCommands = alterMatch[2]; | |
| // Normalize table name | |
| alterTableName = alterTableName.replace(/[`"]/g, '').toLowerCase(); | |
| // Split the commands | |
| const commands = splitAlterCommands(alterCommands); | |
| // Separate ADD COLUMN commands and other commands | |
| const addColumnCommands = []; | |
| const otherCommands = []; | |
| commands.forEach((command) => { | |
| command = command.trim(); | |
| if (/^ADD\s+COLUMN/i.test(command)) { | |
| addColumnCommands.push(command); | |
| } else { | |
| otherCommands.push(command); | |
| } | |
| }); | |
| // Process ADD COLUMN commands first | |
| addColumnCommands.forEach((command) => { | |
| // Process ADD COLUMN | |
| const addColumnMatch = command.match( | |
| /^ADD\s+COLUMN\s+[`"]?([^`"]+)[`"]?\s+(.+)/i | |
| ); | |
| if (addColumnMatch) { | |
| const columnName = addColumnMatch[1]; | |
| let columnDefinition = addColumnMatch[2]; | |
| // Remove 'AFTER' or 'FIRST' if present, as SQLite doesn't support column ordering | |
| columnDefinition = columnDefinition.replace( | |
| /\s+(AFTER|FIRST)\s+[`"]?[^`"]*[`"]?/i, | |
| '' | |
| ); | |
| // Remove UNSIGNED keyword | |
| columnDefinition = columnDefinition.replace(/UNSIGNED|unsigned/g, ''); | |
| // Replace integer types with INTEGER | |
| columnDefinition = columnDefinition.replace( | |
| /\b(TINYINT|SMALLINT|MEDIUMINT|INT|INTEGER|BIGINT)\b/gi, | |
| 'INTEGER' | |
| ); | |
| // Replace AUTO_INCREMENT with PRIMARY KEY AUTOINCREMENT if applicable | |
| if (/AUTO_INCREMENT|auto_increment/i.test(columnDefinition)) { | |
| if (!primaryKeyDefined[alterTableName]) { | |
| columnDefinition = columnDefinition.replace( | |
| /AUTO_INCREMENT|auto_increment/gi, | |
| 'PRIMARY KEY AUTOINCREMENT' | |
| ); | |
| primaryKeyDefined[alterTableName] = true; | |
| } else { | |
| // Remove AUTO_INCREMENT, issue warning | |
| columnDefinition = columnDefinition.replace( | |
| /AUTO_INCREMENT|auto_increment/gi, | |
| '' | |
| ); | |
| printerr( | |
| `WARN: Cannot set ${columnName} as PRIMARY KEY AUTOINCREMENT in table ${alterTableName} because a primary key already exists.` | |
| ); | |
| } | |
| } | |
| // Check if the column already exists | |
| let columnExists = false; | |
| if (tableDefinitions[alterTableName]) { | |
| for (let line of tableDefinitions[alterTableName]) { | |
| if ( | |
| new RegExp(`^[ \\t]*["\`]${columnName}["\`]`, 'i').test( | |
| line.trim() | |
| ) | |
| ) { | |
| columnExists = true; | |
| break; | |
| } | |
| } | |
| } | |
| if (!columnExists) { | |
| // Add the column definition to the CREATE TABLE statement | |
| if (tableDefinitions[alterTableName]) { | |
| let found = false; | |
| for (let i = 0; i < tableDefinitions[alterTableName].length; i++) { | |
| if (tableDefinitions[alterTableName][i].trim() === ');') { | |
| // Insert before the closing ); | |
| // Ensure previous line ends with ',' | |
| if (!tableDefinitions[alterTableName][i - 1].trim().endsWith(',')) { | |
| tableDefinitions[alterTableName][i - 1] += ','; | |
| } | |
| tableDefinitions[alterTableName].splice( | |
| i, | |
| 0, | |
| ` "${columnName}" ${columnDefinition}` | |
| ); | |
| // Add column to tableColumns | |
| if (!tableColumns[alterTableName]) { | |
| tableColumns[alterTableName] = new Set(); | |
| } | |
| tableColumns[alterTableName].add(columnName); | |
| found = true; | |
| break; | |
| } | |
| } | |
| if (!found) { | |
| printerr( | |
| `WARN: Could not find end of CREATE TABLE for ${alterTableName} to add COLUMN ${columnName}.` | |
| ); | |
| } | |
| } else { | |
| printerr( | |
| `WARN: Table ${alterTableName} not found for ALTER TABLE ADD COLUMN.` | |
| ); | |
| } | |
| } else { | |
| printerr( | |
| `INFO: Column ${columnName} already exists in table ${alterTableName}, not adding again.` | |
| ); | |
| } | |
| } else { | |
| printerr(`WARN: Could not parse ADD COLUMN command: ${command}`); | |
| } | |
| }); | |
| // Now process other commands | |
| otherCommands.forEach((command) => { | |
| command = command.trim(); | |
| if (/^ADD PRIMARY KEY/i.test(command)) { | |
| // Process ADD PRIMARY KEY | |
| const pkMatch = command.match(/^ADD PRIMARY KEY\s*\((.+)\)/i); | |
| if (pkMatch) { | |
| const primaryKey = pkMatch[1]; | |
| // Find the table definition and add the primary key | |
| if (tableDefinitions[alterTableName]) { | |
| // Modify the table definition to include the primary key | |
| if (!primaryKeyDefined[alterTableName]) { | |
| let found = false; | |
| for (let i = 0; i < tableDefinitions[alterTableName].length; i++) { | |
| if (tableDefinitions[alterTableName][i].trim() === ');') { | |
| // Insert before the closing ); | |
| // Check if the line before ');' ends with ',', if not, add ',' | |
| if ( | |
| !tableDefinitions[alterTableName][i - 1].trim().endsWith(',') | |
| ) { | |
| tableDefinitions[alterTableName][i - 1] += ','; | |
| } | |
| tableDefinitions[alterTableName][i - 1] += `\n PRIMARY KEY (${primaryKey})`; | |
| primaryKeyDefined[alterTableName] = true; | |
| found = true; | |
| break; | |
| } | |
| } | |
| if (!found) { | |
| printerr( | |
| `WARN: Could not find end of CREATE TABLE for ${alterTableName} to add PRIMARY KEY.` | |
| ); | |
| } | |
| } else { | |
| printerr( | |
| `WARN: Multiple PRIMARY KEY definitions for table ${alterTableName}.` | |
| ); | |
| } | |
| } else { | |
| printerr( | |
| `WARN: Table ${alterTableName} not found for ALTER TABLE ADD PRIMARY KEY.` | |
| ); | |
| } | |
| } | |
| } else if (/^ADD (UNIQUE )?(KEY|INDEX)/i.test(command)) { | |
| // Process ADD KEY | |
| const keyMatch = command.match( | |
| /^ADD (UNIQUE )?(?:KEY|INDEX) [`"]?([^`"]*)[`"]? \((.+)\)/i | |
| ); | |
| if (keyMatch) { | |
| const unique = keyMatch[1] ? 'UNIQUE ' : ''; | |
| let indexName = keyMatch[2]; | |
| if (!indexName) { | |
| indexName = 'auto_index'; | |
| } | |
| const indexColumnsStr = keyMatch[3]; | |
| // Split indexColumnsStr into individual columns and clean them | |
| const indexColumns = indexColumnsStr | |
| .split(',') | |
| .map((col) => col.trim().replace(/[`"]/g, '')); | |
| // Check if all columns exist in the table | |
| let allColumnsExist = true; | |
| indexColumns.forEach((col) => { | |
| if ( | |
| !tableColumns[alterTableName] || | |
| !tableColumns[alterTableName].has(col) | |
| ) { | |
| allColumnsExist = false; | |
| printerr( | |
| `WARN: Column '${col}' in index '${indexName}' does not exist in table '${alterTableName}'. Index will not be created.` | |
| ); | |
| } | |
| }); | |
| if (allColumnsExist) { | |
| // Create a CREATE INDEX statement | |
| const indexStatement = | |
| 'CREATE ' + | |
| unique + | |
| 'INDEX "' + | |
| alterTableName + | |
| '_' + | |
| indexName + | |
| '" ON "' + | |
| alterTableName + | |
| '" (' + | |
| indexColumnsStr + | |
| ');'; | |
| // Store it to be printed later | |
| if (!key[alterTableName]) { | |
| key[alterTableName] = ''; | |
| } | |
| key[alterTableName] += indexStatement + '\n'; | |
| } else { | |
| // Skip creating this index | |
| } | |
| } | |
| } else if (/^ADD CONSTRAINT/i.test(command)) { | |
| // Process ADD CONSTRAINT | |
| const fkMatch = command.match( | |
| /^ADD CONSTRAINT [`"]?([^`"]*)[`"]? FOREIGN KEY\s*\(([^)]+)\)\s*REFERENCES\s+[`"]?([^`"]*)[`"]?\s*\(([^)]+)\)/i | |
| ); | |
| if (fkMatch) { | |
| const constraintName = fkMatch[1]; | |
| const localColumns = fkMatch[2]; | |
| const referencedTable = fkMatch[3]; | |
| const referencedColumns = fkMatch[4]; | |
| // Split local columns in case there are multiple | |
| const localColsArray = localColumns | |
| .split(',') | |
| .map((col) => col.trim().replace(/[`"]/g, '')); | |
| // Ensure all local columns exist in the table | |
| let allColumnsExist = true; | |
| localColsArray.forEach((columnName) => { | |
| if ( | |
| !tableColumns[alterTableName] || | |
| !tableColumns[alterTableName].has(columnName) | |
| ) { | |
| allColumnsExist = false; | |
| printerr( | |
| `WARN: Column '${columnName}' in FOREIGN KEY constraint does not exist in table '${alterTableName}'. Constraint will not be created.` | |
| ); | |
| } | |
| }); | |
| if (allColumnsExist) { | |
| // Build FOREIGN KEY constraint | |
| const foreignKeyStatement = `FOREIGN KEY (${localColumns}) REFERENCES ${referencedTable}(${referencedColumns})`; | |
| // Now, we need to add this constraint to the CREATE TABLE statement | |
| if (tableDefinitions[alterTableName]) { | |
| // Find the table definition and add the foreign key constraint | |
| let found = false; | |
| for ( | |
| let i = 0; | |
| i < tableDefinitions[alterTableName].length; | |
| i++ | |
| ) { | |
| if (tableDefinitions[alterTableName][i].trim() === ');') { | |
| // Insert before the closing ); | |
| // Check if the line before ');' ends with ',', if not, add ',' | |
| if ( | |
| !tableDefinitions[alterTableName][i - 1].trim().endsWith(',') | |
| ) { | |
| tableDefinitions[alterTableName][i - 1] += ','; | |
| } | |
| tableDefinitions[alterTableName][i - 1] += `\n ${foreignKeyStatement}`; | |
| found = true; | |
| break; | |
| } | |
| } | |
| if (!found) { | |
| printerr( | |
| `WARN: Could not find end of CREATE TABLE for ${alterTableName} to add FOREIGN KEY.` | |
| ); | |
| } | |
| } else { | |
| printerr( | |
| `WARN: Table ${alterTableName} not found for ALTER TABLE ADD CONSTRAINT FOREIGN KEY.` | |
| ); | |
| } | |
| } else { | |
| // Skip creating this constraint | |
| } | |
| } else { | |
| printerr(`WARN: Could not parse ADD CONSTRAINT command: ${command}`); | |
| } | |
| } else if (/^MODIFY/i.test(command)) { | |
| // Process MODIFY | |
| const modifyMatch = command.match( | |
| /^MODIFY [`"]?([^`"]*)[`"]?\s+(.+)/i | |
| ); | |
| if (modifyMatch) { | |
| const columnName = modifyMatch[1]; | |
| let columnDefinition = modifyMatch[2]; | |
| // Remove UNSIGNED keyword | |
| columnDefinition = columnDefinition.replace(/UNSIGNED|unsigned/g, ''); | |
| // Replace integer types with INTEGER | |
| columnDefinition = columnDefinition.replace( | |
| /\b(TINYINT|SMALLINT|MEDIUMINT|INT|INTEGER|BIGINT)\b/gi, | |
| 'INTEGER' | |
| ); | |
| // Replace AUTO_INCREMENT with PRIMARY KEY AUTOINCREMENT if applicable | |
| if (/AUTO_INCREMENT|auto_increment/i.test(columnDefinition)) { | |
| if (!primaryKeyDefined[alterTableName]) { | |
| columnDefinition = columnDefinition.replace( | |
| /AUTO_INCREMENT|auto_increment/gi, | |
| 'PRIMARY KEY AUTOINCREMENT' | |
| ); | |
| primaryKeyDefined[alterTableName] = true; | |
| } else { | |
| // Remove AUTO_INCREMENT, issue warning | |
| columnDefinition = columnDefinition.replace( | |
| /AUTO_INCREMENT|auto_increment/gi, | |
| '' | |
| ); | |
| printerr( | |
| `WARN: Cannot set ${columnName} as PRIMARY KEY AUTOINCREMENT in table ${alterTableName} because a primary key already exists.` | |
| ); | |
| } | |
| } | |
| // Find the column in the CREATE TABLE statement and modify its definition | |
| if (tableDefinitions[alterTableName]) { | |
| let found = false; | |
| for ( | |
| let i = 0; | |
| i < tableDefinitions[alterTableName].length; | |
| i++ | |
| ) { | |
| let line = tableDefinitions[alterTableName][i]; | |
| // Match the column definition | |
| const columnMatch = line.match( | |
| new RegExp(`^[ \\t]*[\`"]?${columnName}[\`"]?[ \\t]+.*`, 'i') | |
| ); | |
| if (columnMatch) { | |
| // Modify the line to have the new column definition | |
| tableDefinitions[alterTableName][i] = | |
| ` "${columnName}" ${columnDefinition}`; | |
| found = true; | |
| break; | |
| } | |
| } | |
| if (!found) { | |
| printerr( | |
| `WARN: Column ${columnName} not found in table ${alterTableName} for MODIFY command.` | |
| ); | |
| } | |
| } else { | |
| printerr( | |
| `WARN: Table ${alterTableName} not found for ALTER TABLE MODIFY.` | |
| ); | |
| } | |
| } else { | |
| printerr(`WARN: Could not parse MODIFY command: ${command}`); | |
| } | |
| } else { | |
| printerr(`WARN: Unhandled ALTER TABLE command: ${command}`); | |
| } | |
| }); | |
| } else { | |
| printerr( | |
| `WARN: Could not parse ALTER TABLE statement: ${alterTableStatement}` | |
| ); | |
| } | |
| return; | |
| } | |
| return; | |
| } | |
| }); | |
| // Function to split ALTER TABLE commands, considering commas inside parentheses | |
| function splitAlterCommands(alterCommands) { | |
| let commands = []; | |
| let currentCommand = ''; | |
| let parenLevel = 0; | |
| let inQuote = false; | |
| let quoteChar = ''; | |
| for (let i = 0; i < alterCommands.length; i++) { | |
| let c = alterCommands[i]; | |
| currentCommand += c; | |
| if (c === '\'' || c === '"' || c === '`') { | |
| if (!inQuote) { | |
| inQuote = true; | |
| quoteChar = c; | |
| } else if (c === quoteChar) { | |
| inQuote = false; | |
| quoteChar = ''; | |
| } | |
| } else if (!inQuote) { | |
| if (c === '(') { | |
| parenLevel++; | |
| } else if (c === ')') { | |
| parenLevel--; | |
| } else if (c === ',' && parenLevel === 0) { | |
| // Found a top-level comma, split here | |
| commands.push(currentCommand.trim()); | |
| currentCommand = ''; | |
| } | |
| } | |
| } | |
| if (currentCommand.trim() !== '') { | |
| commands.push(currentCommand.trim()); | |
| } | |
| return commands; | |
| } | |
| rl.on('close', () => { | |
| // Process any remaining 'prev' content | |
| if (prev && currentTable && tableDefinitions[currentTable]) { | |
| if (firstInTable) { | |
| tableDefinitions[currentTable].push(prev); | |
| firstInTable = false; | |
| } else { | |
| tableDefinitions[currentTable].push(',' + prev); | |
| } | |
| prev = ''; | |
| } | |
| // Ensure commas are correctly placed in CREATE TABLE statements | |
| for (const table in tableDefinitions) { | |
| const lines = tableDefinitions[table]; | |
| // Remove any extra commas before closing ); | |
| for (let i = lines.length - 1; i >= 0; i--) { | |
| if (lines[i].trim() === ');') { | |
| if (i > 0 && lines[i - 1].trim().endsWith(',')) { | |
| lines[i - 1] = lines[i - 1].trim().slice(0, -1); | |
| } | |
| break; | |
| } | |
| } | |
| } | |
| // Output modified CREATE TABLE statements | |
| for (const table in tableDefinitions) { | |
| tableDefinitions[table].forEach((line) => { | |
| console.log(line); | |
| }); | |
| } | |
| // Print all accumulated KEY definitions | |
| for (const table in key) { | |
| process.stdout.write(key[table]); | |
| } | |
| // End transaction | |
| console.log('END TRANSACTION;'); | |
| // Print case sensitivity warning if necessary | |
| if (caseIssue) { | |
| printerr( | |
| 'INFO Pure sqlite identifiers are case insensitive (even if quoted\n' + | |
| ' or if ASCII) and doesnt cross-check TABLE and TEMPORARY TABLE\n' + | |
| ' identifiers. Thus expect errors like "table T has no column named F".' | |
| ); | |
| } | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment