diff --git a/src/lib/data/export-metadata/export-per-type/mssql.ts b/src/lib/data/export-metadata/export-per-type/mssql.ts index cd593a50..84484161 100644 --- a/src/lib/data/export-metadata/export-per-type/mssql.ts +++ b/src/lib/data/export-metadata/export-per-type/mssql.ts @@ -73,7 +73,13 @@ function parseMSSQLDefault(field: DBField): string { return `'${defaultValue}'`; } -export function exportMSSQL(diagram: Diagram): string { +export function exportMSSQL({ + diagram, + onlyRelationships = false, +}: { + diagram: Diagram; + onlyRelationships?: boolean; +}): string { if (!diagram.tables || !diagram.relationships) { return ''; } @@ -83,134 +89,139 @@ export function exportMSSQL(diagram: Diagram): string { // Create CREATE SCHEMA statements for all schemas let sqlScript = ''; - const schemas = new Set(); - tables.forEach((table) => { - if (table.schema) { - schemas.add(table.schema); - } - }); + if (!onlyRelationships) { + const schemas = new Set(); - // Add schema creation statements - schemas.forEach((schema) => { - sqlScript += `IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${schema}')\nBEGIN\n EXEC('CREATE SCHEMA [${schema}]');\nEND;\n`; - }); - - // Generate table creation SQL - sqlScript += tables - .map((table: DBTable) => { - // Skip views - if (table.isView) { - return ''; + tables.forEach((table) => { + if (table.schema) { + schemas.add(table.schema); } + }); - const tableName = table.schema - ? `[${table.schema}].[${table.name}]` - : `[${table.name}]`; + // Add schema creation statements + schemas.forEach((schema) => { + sqlScript += `IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${schema}')\nBEGIN\n EXEC('CREATE SCHEMA [${schema}]');\nEND;\n`; + }); - return `${ - table.comments ? formatMSSQLTableComment(table.comments) : '' - }CREATE TABLE ${tableName} (\n${table.fields - .map((field: DBField) => { - const fieldName = `[${field.name}]`; - const typeName = field.type.name; + // Generate table creation SQL + sqlScript += tables + .map((table: DBTable) => { + // Skip views + if (table.isView) { + return ''; + } - // Handle SQL Server specific type formatting - let typeWithSize = typeName; - if (field.characterMaximumLength) { - if ( - typeName.toLowerCase() === 'varchar' || - typeName.toLowerCase() === 'nvarchar' || - typeName.toLowerCase() === 'char' || - typeName.toLowerCase() === 'nchar' - ) { - typeWithSize = `${typeName}(${field.characterMaximumLength})`; + const tableName = table.schema + ? `[${table.schema}].[${table.name}]` + : `[${table.name}]`; + + return `${ + table.comments + ? formatMSSQLTableComment(table.comments) + : '' + }CREATE TABLE ${tableName} (\n${table.fields + .map((field: DBField) => { + const fieldName = `[${field.name}]`; + const typeName = field.type.name; + + // Handle SQL Server specific type formatting + let typeWithSize = typeName; + if (field.characterMaximumLength) { + if ( + typeName.toLowerCase() === 'varchar' || + typeName.toLowerCase() === 'nvarchar' || + typeName.toLowerCase() === 'char' || + typeName.toLowerCase() === 'nchar' + ) { + typeWithSize = `${typeName}(${field.characterMaximumLength})`; + } + } else if (field.precision && field.scale) { + if ( + typeName.toLowerCase() === 'decimal' || + typeName.toLowerCase() === 'numeric' + ) { + typeWithSize = `${typeName}(${field.precision}, ${field.scale})`; + } + } else if (field.precision) { + if ( + typeName.toLowerCase() === 'decimal' || + typeName.toLowerCase() === 'numeric' + ) { + typeWithSize = `${typeName}(${field.precision})`; + } } - } else if (field.precision && field.scale) { - if ( - typeName.toLowerCase() === 'decimal' || - typeName.toLowerCase() === 'numeric' - ) { - typeWithSize = `${typeName}(${field.precision}, ${field.scale})`; - } - } else if (field.precision) { - if ( - typeName.toLowerCase() === 'decimal' || - typeName.toLowerCase() === 'numeric' - ) { - typeWithSize = `${typeName}(${field.precision})`; - } - } - const notNull = field.nullable ? '' : ' NOT NULL'; + const notNull = field.nullable ? '' : ' NOT NULL'; - // Check if identity column - const identity = field.default - ?.toLowerCase() - .includes('identity') - ? ' IDENTITY(1,1)' - : ''; - - const unique = - !field.primaryKey && field.unique ? ' UNIQUE' : ''; - - // Handle default value using SQL Server specific parser - const defaultValue = - field.default && - !field.default.toLowerCase().includes('identity') - ? ` DEFAULT ${parseMSSQLDefault(field)}` + // Check if identity column + const identity = field.default + ?.toLowerCase() + .includes('identity') + ? ' IDENTITY(1,1)' : ''; - // Do not add PRIMARY KEY as a column constraint - will add as table constraint - return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${identity}${unique}${defaultValue}`; - }) - .join(',\n')}${ - table.fields.filter((f) => f.primaryKey).length > 0 - ? `,\n PRIMARY KEY (${table.fields - .filter((f) => f.primaryKey) - .map((f) => `[${f.name}]`) - .join(', ')})` - : '' - }\n);\n${(() => { - const validIndexes = table.indexes - .map((index) => { - const indexName = table.schema - ? `[${table.schema}_${index.name}]` - : `[${index.name}]`; - const indexFields = index.fieldIds - .map((fieldId) => { - const field = table.fields.find( - (f) => f.id === fieldId - ); - return field ? `[${field.name}]` : ''; - }) - .filter(Boolean); + const unique = + !field.primaryKey && field.unique ? ' UNIQUE' : ''; - // SQL Server has a limit of 32 columns in an index - if (indexFields.length > 32) { - const warningComment = `/* WARNING: This index originally had ${indexFields.length} columns. It has been truncated to 32 columns due to SQL Server's index column limit. */\n`; - console.warn( - `Warning: Index ${indexName} on table ${tableName} has ${indexFields.length} columns. SQL Server limits indexes to 32 columns. The index will be truncated.` - ); - indexFields.length = 32; - return indexFields.length > 0 - ? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});` + // Handle default value using SQL Server specific parser + const defaultValue = + field.default && + !field.default.toLowerCase().includes('identity') + ? ` DEFAULT ${parseMSSQLDefault(field)}` : ''; - } - return indexFields.length > 0 - ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});` - : ''; + // Do not add PRIMARY KEY as a column constraint - will add as table constraint + return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${identity}${unique}${defaultValue}`; }) - .filter(Boolean); + .join(',\n')}${ + table.fields.filter((f) => f.primaryKey).length > 0 + ? `,\n PRIMARY KEY (${table.fields + .filter((f) => f.primaryKey) + .map((f) => `[${f.name}]`) + .join(', ')})` + : '' + }\n);\n${(() => { + const validIndexes = table.indexes + .map((index) => { + const indexName = table.schema + ? `[${table.schema}_${index.name}]` + : `[${index.name}]`; + const indexFields = index.fieldIds + .map((fieldId) => { + const field = table.fields.find( + (f) => f.id === fieldId + ); + return field ? `[${field.name}]` : ''; + }) + .filter(Boolean); - return validIndexes.length > 0 - ? `\n-- Indexes\n${validIndexes.join('\n')}` - : ''; - })()}\n`; - }) - .filter(Boolean) // Remove empty strings (views) - .join('\n'); + // SQL Server has a limit of 32 columns in an index + if (indexFields.length > 32) { + const warningComment = `/* WARNING: This index originally had ${indexFields.length} columns. It has been truncated to 32 columns due to SQL Server's index column limit. */\n`; + console.warn( + `Warning: Index ${indexName} on table ${tableName} has ${indexFields.length} columns. SQL Server limits indexes to 32 columns. The index will be truncated.` + ); + indexFields.length = 32; + return indexFields.length > 0 + ? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});` + : ''; + } + + return indexFields.length > 0 + ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});` + : ''; + }) + .filter(Boolean); + + return validIndexes.length > 0 + ? `\n-- Indexes\n${validIndexes.join('\n')}` + : ''; + })()}\n`; + }) + .filter(Boolean) // Remove empty strings (views) + .join('\n'); + } // Generate foreign keys if (relationships.length > 0) { diff --git a/src/lib/data/export-metadata/export-per-type/mysql.ts b/src/lib/data/export-metadata/export-per-type/mysql.ts index acf99d63..ba31594c 100644 --- a/src/lib/data/export-metadata/export-per-type/mysql.ts +++ b/src/lib/data/export-metadata/export-per-type/mysql.ts @@ -170,7 +170,13 @@ function mapMySQLType(typeName: string): string { return typeName; } -export function exportMySQL(diagram: Diagram): string { +export function exportMySQL({ + diagram, + onlyRelationships = false, +}: { + diagram: Diagram; + onlyRelationships?: boolean; +}): string { if (!diagram.tables || !diagram.relationships) { return ''; } @@ -181,226 +187,236 @@ export function exportMySQL(diagram: Diagram): string { // Start SQL script let sqlScript = '-- MySQL database export\n'; - // MySQL doesn't really use transactions for DDL statements but we'll add it for consistency - sqlScript += 'START TRANSACTION;\n'; + if (!onlyRelationships) { + // MySQL doesn't really use transactions for DDL statements but we'll add it for consistency + sqlScript += 'START TRANSACTION;\n'; - // Create databases (schemas) if they don't exist - const schemas = new Set(); - tables.forEach((table) => { - if (table.schema) { - schemas.add(table.schema); - } - }); - - schemas.forEach((schema) => { - sqlScript += `CREATE DATABASE IF NOT EXISTS \`${schema}\`;\n`; - }); - - if (schemas.size > 0) { - sqlScript += '\n'; - } - - // Generate table creation SQL - sqlScript += tables - .map((table: DBTable) => { - // Skip views - if (table.isView) { - return ''; + // Create databases (schemas) if they don't exist + const schemas = new Set(); + tables.forEach((table) => { + if (table.schema) { + schemas.add(table.schema); } + }); - // Use schema prefix if available - const tableName = table.schema - ? `\`${table.schema}\`.\`${table.name}\`` - : `\`${table.name}\``; + schemas.forEach((schema) => { + sqlScript += `CREATE DATABASE IF NOT EXISTS \`${schema}\`;\n`; + }); - // Get primary key fields - const primaryKeyFields = table.fields.filter((f) => f.primaryKey); + if (schemas.size > 0) { + sqlScript += '\n'; + } - return `${ - table.comments ? formatTableComment(table.comments) : '' - }\nCREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields - .map((field: DBField) => { - const fieldName = `\`${field.name}\``; + // Generate table creation SQL + sqlScript += tables + .map((table: DBTable) => { + // Skip views + if (table.isView) { + return ''; + } - // Handle type name - map to MySQL compatible types - const typeName = mapMySQLType(field.type.name); + // Use schema prefix if available + const tableName = table.schema + ? `\`${table.schema}\`.\`${table.name}\`` + : `\`${table.name}\``; - // Handle MySQL specific type formatting - let typeWithSize = typeName; - if (field.characterMaximumLength) { - if ( - typeName.toLowerCase() === 'varchar' || - typeName.toLowerCase() === 'char' || - typeName.toLowerCase() === 'varbinary' - ) { - typeWithSize = `${typeName}(${field.characterMaximumLength})`; + // Get primary key fields + const primaryKeyFields = table.fields.filter( + (f) => f.primaryKey + ); + + return `${ + table.comments ? formatTableComment(table.comments) : '' + }\nCREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields + .map((field: DBField) => { + const fieldName = `\`${field.name}\``; + + // Handle type name - map to MySQL compatible types + const typeName = mapMySQLType(field.type.name); + + // Handle MySQL specific type formatting + let typeWithSize = typeName; + if (field.characterMaximumLength) { + if ( + typeName.toLowerCase() === 'varchar' || + typeName.toLowerCase() === 'char' || + typeName.toLowerCase() === 'varbinary' + ) { + typeWithSize = `${typeName}(${field.characterMaximumLength})`; + } + } else if (field.precision && field.scale) { + if ( + typeName.toLowerCase() === 'decimal' || + typeName.toLowerCase() === 'numeric' + ) { + typeWithSize = `${typeName}(${field.precision}, ${field.scale})`; + } + } else if (field.precision) { + if ( + typeName.toLowerCase() === 'decimal' || + typeName.toLowerCase() === 'numeric' + ) { + typeWithSize = `${typeName}(${field.precision})`; + } } - } else if (field.precision && field.scale) { + + // Set a default size for VARCHAR columns if not specified if ( - typeName.toLowerCase() === 'decimal' || - typeName.toLowerCase() === 'numeric' + typeName.toLowerCase() === 'varchar' && + !field.characterMaximumLength ) { - typeWithSize = `${typeName}(${field.precision}, ${field.scale})`; + typeWithSize = `${typeName}(255)`; } - } else if (field.precision) { + + const notNull = field.nullable ? '' : ' NOT NULL'; + + // Handle auto_increment - MySQL uses AUTO_INCREMENT keyword + let autoIncrement = ''; if ( - typeName.toLowerCase() === 'decimal' || - typeName.toLowerCase() === 'numeric' - ) { - typeWithSize = `${typeName}(${field.precision})`; - } - } - - // Set a default size for VARCHAR columns if not specified - if ( - typeName.toLowerCase() === 'varchar' && - !field.characterMaximumLength - ) { - typeWithSize = `${typeName}(255)`; - } - - const notNull = field.nullable ? '' : ' NOT NULL'; - - // Handle auto_increment - MySQL uses AUTO_INCREMENT keyword - let autoIncrement = ''; - if ( - field.primaryKey && - (field.default?.toLowerCase().includes('identity') || - field.default + field.primaryKey && + (field.default ?.toLowerCase() - .includes('autoincrement') || - field.default?.includes('nextval')) - ) { - autoIncrement = ' AUTO_INCREMENT'; - } + .includes('identity') || + field.default + ?.toLowerCase() + .includes('autoincrement') || + field.default?.includes('nextval')) + ) { + autoIncrement = ' AUTO_INCREMENT'; + } - // Only add UNIQUE constraint if the field is not part of the primary key - const unique = - !field.primaryKey && field.unique ? ' UNIQUE' : ''; + // Only add UNIQUE constraint if the field is not part of the primary key + const unique = + !field.primaryKey && field.unique ? ' UNIQUE' : ''; - // Handle default value - const defaultValue = - field.default && - !field.default.toLowerCase().includes('identity') && - !field.default - .toLowerCase() - .includes('autoincrement') && - !field.default.includes('nextval') - ? ` DEFAULT ${parseMySQLDefault(field)}` + // Handle default value + const defaultValue = + field.default && + !field.default.toLowerCase().includes('identity') && + !field.default + .toLowerCase() + .includes('autoincrement') && + !field.default.includes('nextval') + ? ` DEFAULT ${parseMySQLDefault(field)}` + : ''; + + // MySQL supports inline comments + const comment = field.comments + ? ` COMMENT '${escapeSQLComment(field.comments)}'` : ''; - // MySQL supports inline comments - const comment = field.comments - ? ` COMMENT '${escapeSQLComment(field.comments)}'` - : ''; + return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${autoIncrement}${unique}${defaultValue}${comment}`; + }) + .join(',\n')}${ + // Add PRIMARY KEY as table constraint + primaryKeyFields.length > 0 + ? `,\n PRIMARY KEY (${primaryKeyFields + .map((f) => `\`${f.name}\``) + .join(', ')})` + : '' + }\n)${ + // MySQL supports table comments + table.comments + ? ` COMMENT='${escapeSQLComment(table.comments)}'` + : '' + };\n${ + // Add indexes - MySQL creates them separately from the table definition + (() => { + const validIndexes = table.indexes + .map((index) => { + // Get the list of fields for this index + const indexFields = index.fieldIds + .map((fieldId) => { + const field = table.fields.find( + (f) => f.id === fieldId + ); + return field ? field : null; + }) + .filter(Boolean); - return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${autoIncrement}${unique}${defaultValue}${comment}`; - }) - .join(',\n')}${ - // Add PRIMARY KEY as table constraint - primaryKeyFields.length > 0 - ? `,\n PRIMARY KEY (${primaryKeyFields - .map((f) => `\`${f.name}\``) - .join(', ')})` - : '' - }\n)${ - // MySQL supports table comments - table.comments - ? ` COMMENT='${escapeSQLComment(table.comments)}'` - : '' - };\n${ - // Add indexes - MySQL creates them separately from the table definition - (() => { - const validIndexes = table.indexes - .map((index) => { - // Get the list of fields for this index - const indexFields = index.fieldIds - .map((fieldId) => { - const field = table.fields.find( - (f) => f.id === fieldId - ); - return field ? field : null; - }) - .filter(Boolean); - - // Skip if this index exactly matches the primary key fields - if ( - primaryKeyFields.length === - indexFields.length && - primaryKeyFields.every((pk) => - indexFields.some( - (field) => field && field.id === pk.id + // Skip if this index exactly matches the primary key fields + if ( + primaryKeyFields.length === + indexFields.length && + primaryKeyFields.every((pk) => + indexFields.some( + (field) => + field && field.id === pk.id + ) ) - ) - ) { - return ''; - } + ) { + return ''; + } - // Create a unique index name by combining table name, field names, and a unique/non-unique indicator - const fieldNamesForIndex = indexFields - .map((field) => field?.name || '') - .join('_'); - const uniqueIndicator = index.unique - ? '_unique' - : ''; - const indexName = `\`idx_${table.name}_${fieldNamesForIndex}${uniqueIndicator}\``; + // Create a unique index name by combining table name, field names, and a unique/non-unique indicator + const fieldNamesForIndex = indexFields + .map((field) => field?.name || '') + .join('_'); + const uniqueIndicator = index.unique + ? '_unique' + : ''; + const indexName = `\`idx_${table.name}_${fieldNamesForIndex}${uniqueIndicator}\``; - // Get the properly quoted field names - const indexFieldNames = indexFields - .map((field) => - field ? `\`${field.name}\`` : '' - ) - .filter(Boolean); + // Get the properly quoted field names + const indexFieldNames = indexFields + .map((field) => + field ? `\`${field.name}\`` : '' + ) + .filter(Boolean); - // Check for text/blob fields that need special handling - const hasTextOrBlob = indexFields.some((field) => { - const typeName = - field?.type.name.toLowerCase() || ''; - return ( - typeName === 'text' || - typeName === 'mediumtext' || - typeName === 'longtext' || - typeName === 'blob' + // Check for text/blob fields that need special handling + const hasTextOrBlob = indexFields.some( + (field) => { + const typeName = + field?.type.name.toLowerCase() || + ''; + return ( + typeName === 'text' || + typeName === 'mediumtext' || + typeName === 'longtext' || + typeName === 'blob' + ); + } ); - }); - // If there are TEXT/BLOB fields, need to add prefix length - const indexFieldsWithPrefix = hasTextOrBlob - ? indexFieldNames.map((name) => { - const field = indexFields.find( - (f) => `\`${f?.name}\`` === name - ); - if (!field) return name; + // If there are TEXT/BLOB fields, need to add prefix length + const indexFieldsWithPrefix = hasTextOrBlob + ? indexFieldNames.map((name) => { + const field = indexFields.find( + (f) => `\`${f?.name}\`` === name + ); + if (!field) return name; - const typeName = - field.type.name.toLowerCase(); - if ( - typeName === 'text' || - typeName === 'mediumtext' || - typeName === 'longtext' || - typeName === 'blob' - ) { - // Add a prefix length for TEXT/BLOB fields (required in MySQL) - return `${name}(255)`; - } - return name; - }) - : indexFieldNames; + const typeName = + field.type.name.toLowerCase(); + if ( + typeName === 'text' || + typeName === 'mediumtext' || + typeName === 'longtext' || + typeName === 'blob' + ) { + // Add a prefix length for TEXT/BLOB fields (required in MySQL) + return `${name}(255)`; + } + return name; + }) + : indexFieldNames; - return indexFieldNames.length > 0 - ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${indexFieldsWithPrefix.join(', ')});` - : ''; - }) - .filter(Boolean); + return indexFieldNames.length > 0 + ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${indexFieldsWithPrefix.join(', ')});` + : ''; + }) + .filter(Boolean); - return validIndexes.length > 0 - ? `\n-- Indexes\n${validIndexes.join('\n')}` - : ''; - })() - }\n`; - }) - .filter(Boolean) // Remove empty strings (views) - .join('\n'); + return validIndexes.length > 0 + ? `\n-- Indexes\n${validIndexes.join('\n')}` + : ''; + })() + }\n`; + }) + .filter(Boolean) // Remove empty strings (views) + .join('\n'); + } // Generate foreign keys if (relationships.length > 0) { diff --git a/src/lib/data/export-metadata/export-per-type/postgresql.ts b/src/lib/data/export-metadata/export-per-type/postgresql.ts index a7084d66..2636ae28 100644 --- a/src/lib/data/export-metadata/export-per-type/postgresql.ts +++ b/src/lib/data/export-metadata/export-per-type/postgresql.ts @@ -145,7 +145,13 @@ function exportCustomTypes(customTypes: DBCustomType[]): string { return typesSql ? typesSql + '\n' : ''; } -export function exportPostgreSQL(diagram: Diagram): string { +export function exportPostgreSQL({ + diagram, + onlyRelationships = false, +}: { + diagram: Diagram; + onlyRelationships?: boolean; +}): string { if (!diagram.tables || !diagram.relationships) { return ''; } @@ -156,252 +162,262 @@ export function exportPostgreSQL(diagram: Diagram): string { // Create CREATE SCHEMA statements for all schemas let sqlScript = ''; - const schemas = new Set(); + if (!onlyRelationships) { + const schemas = new Set(); - tables.forEach((table) => { - if (table.schema) { - schemas.add(table.schema); - } - }); - - // Also collect schemas from custom types - customTypes.forEach((customType) => { - if (customType.schema) { - schemas.add(customType.schema); - } - }); - - // Add schema creation statements - schemas.forEach((schema) => { - sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`; - }); - if (schemas.size > 0) { - sqlScript += '\n'; - } - - // Add custom types (enums and composite types) - sqlScript += exportCustomTypes(customTypes); - - // Add sequence creation statements - const sequences = new Set(); - - tables.forEach((table) => { - table.fields.forEach((field) => { - if (field.default) { - // Match nextval('schema.sequence_name') or nextval('sequence_name') - const match = field.default.match( - /nextval\('([^']+)'(?:::[^)]+)?\)/ - ); - if (match) { - sequences.add(match[1]); - } + tables.forEach((table) => { + if (table.schema) { + schemas.add(table.schema); } }); - }); - sequences.forEach((sequence) => { - sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`; - }); - if (sequences.size > 0) { - sqlScript += '\n'; - } - - // Generate table creation SQL - sqlScript += tables - .map((table: DBTable) => { - // Skip views - if (table.isView) { - return ''; + // Also collect schemas from custom types + customTypes.forEach((customType) => { + if (customType.schema) { + schemas.add(customType.schema); } + }); - const tableName = table.schema - ? `"${table.schema}"."${table.name}"` - : `"${table.name}"`; + // Add schema creation statements + schemas.forEach((schema) => { + sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`; + }); + if (schemas.size > 0) { + sqlScript += '\n'; + } - // Get primary key fields - const primaryKeyFields = table.fields.filter((f) => f.primaryKey); + // Add custom types (enums and composite types) + sqlScript += exportCustomTypes(customTypes); - return `${ - table.comments ? formatTableComment(table.comments) : '' - }CREATE TABLE ${tableName} (\n${table.fields - .map((field: DBField) => { - const fieldName = `"${field.name}"`; + // Add sequence creation statements + const sequences = new Set(); - // Handle type name - map problematic types to PostgreSQL compatible types - const typeName = mapPostgresType( - field.type.name, - field.name + tables.forEach((table) => { + table.fields.forEach((field) => { + if (field.default) { + // Match nextval('schema.sequence_name') or nextval('sequence_name') + const match = field.default.match( + /nextval\('([^']+)'(?:::[^)]+)?\)/ ); - - // Handle PostgreSQL specific type formatting - let typeWithSize = typeName; - let serialType = null; - - if (field.increment && !field.nullable) { - if ( - typeName.toLowerCase() === 'integer' || - typeName.toLowerCase() === 'int' - ) { - serialType = 'SERIAL'; - } else if (typeName.toLowerCase() === 'bigint') { - serialType = 'BIGSERIAL'; - } else if (typeName.toLowerCase() === 'smallint') { - serialType = 'SMALLSERIAL'; - } + if (match) { + sequences.add(match[1]); } + } + }); + }); - if (field.characterMaximumLength) { - if ( - typeName.toLowerCase() === 'varchar' || - typeName.toLowerCase() === 'character varying' || - typeName.toLowerCase() === 'char' || - typeName.toLowerCase() === 'character' - ) { - typeWithSize = `${typeName}(${field.characterMaximumLength})`; - } - } else if (field.precision && field.scale) { - if ( - typeName.toLowerCase() === 'decimal' || - typeName.toLowerCase() === 'numeric' - ) { - typeWithSize = `${typeName}(${field.precision}, ${field.scale})`; - } - } else if (field.precision) { - if ( - typeName.toLowerCase() === 'decimal' || - typeName.toLowerCase() === 'numeric' - ) { - typeWithSize = `${typeName}(${field.precision})`; - } - } + sequences.forEach((sequence) => { + sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`; + }); + if (sequences.size > 0) { + sqlScript += '\n'; + } - // Handle array types (check if the type name ends with '[]') - if (typeName.endsWith('[]')) { - typeWithSize = typeWithSize.replace('[]', '') + '[]'; - } + // Generate table creation SQL + sqlScript += tables + .map((table: DBTable) => { + // Skip views + if (table.isView) { + return ''; + } - const notNull = field.nullable ? '' : ' NOT NULL'; + const tableName = table.schema + ? `"${table.schema}"."${table.name}"` + : `"${table.name}"`; - // Handle identity generation - let identity = ''; - if (field.default && field.default.includes('nextval')) { - // PostgreSQL already handles this with DEFAULT nextval() - } else if ( - field.default && - field.default.toLowerCase().includes('identity') - ) { - identity = ' GENERATED BY DEFAULT AS IDENTITY'; - } + // Get primary key fields + const primaryKeyFields = table.fields.filter( + (f) => f.primaryKey + ); - // Only add UNIQUE constraint if the field is not part of the primary key - // This avoids redundant uniqueness constraints - const unique = - !field.primaryKey && field.unique ? ' UNIQUE' : ''; + return `${ + table.comments ? formatTableComment(table.comments) : '' + }CREATE TABLE ${tableName} (\n${table.fields + .map((field: DBField) => { + const fieldName = `"${field.name}"`; - // Handle default value using PostgreSQL specific parser - const defaultValue = - field.default && - !field.default.toLowerCase().includes('identity') - ? ` DEFAULT ${parsePostgresDefault(field)}` - : ''; + // Handle type name - map problematic types to PostgreSQL compatible types + const typeName = mapPostgresType( + field.type.name, + field.name + ); - // Do not add PRIMARY KEY as a column constraint - will add as table constraint - return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${serialType || typeWithSize}${serialType ? '' : notNull}${identity}${unique}${defaultValue}`; - }) - .join(',\n')}${ - primaryKeyFields.length > 0 - ? `,\n PRIMARY KEY (${primaryKeyFields - .map((f) => `"${f.name}"`) - .join(', ')})` - : '' - }\n);${ - // Add table comments - table.comments - ? `\nCOMMENT ON TABLE ${tableName} IS '${escapeSQLComment(table.comments)}';` - : '' - }${ - // Add column comments - table.fields - .filter((f) => f.comments) - .map( - (f) => - `\nCOMMENT ON COLUMN ${tableName}."${f.name}" IS '${escapeSQLComment(f.comments || '')}';` - ) - .join('') - }${ - // Add indexes only for non-primary key fields or composite indexes - // This avoids duplicate indexes on primary key columns - (() => { - const validIndexes = table.indexes - .map((index) => { - // Get the list of fields for this index - const indexFields = index.fieldIds - .map((fieldId) => { - const field = table.fields.find( - (f) => f.id === fieldId - ); - return field ? field : null; - }) - .filter(Boolean); + // Handle PostgreSQL specific type formatting + let typeWithSize = typeName; + let serialType = null; - // Skip if this index exactly matches the primary key fields - // This prevents creating redundant indexes + if (field.increment && !field.nullable) { if ( - primaryKeyFields.length === - indexFields.length && - primaryKeyFields.every((pk) => - indexFields.some( - (field) => field && field.id === pk.id - ) - ) + typeName.toLowerCase() === 'integer' || + typeName.toLowerCase() === 'int' ) { - return ''; + serialType = 'SERIAL'; + } else if (typeName.toLowerCase() === 'bigint') { + serialType = 'BIGSERIAL'; + } else if (typeName.toLowerCase() === 'smallint') { + serialType = 'SMALLSERIAL'; } + } - // Create unique index name using table name and index name - // This ensures index names are unique across the database - const safeTableName = table.name.replace( - /[^a-zA-Z0-9_]/g, - '_' - ); - const safeIndexName = index.name.replace( - /[^a-zA-Z0-9_]/g, - '_' - ); - - // Limit index name length to avoid PostgreSQL's 63-character identifier limit - let combinedName = `${safeTableName}_${safeIndexName}`; - if (combinedName.length > 60) { - // If too long, use just the index name or a truncated version - combinedName = - safeIndexName.length > 60 - ? safeIndexName.substring(0, 60) - : safeIndexName; + if (field.characterMaximumLength) { + if ( + typeName.toLowerCase() === 'varchar' || + typeName.toLowerCase() === + 'character varying' || + typeName.toLowerCase() === 'char' || + typeName.toLowerCase() === 'character' + ) { + typeWithSize = `${typeName}(${field.characterMaximumLength})`; } + } else if (field.precision && field.scale) { + if ( + typeName.toLowerCase() === 'decimal' || + typeName.toLowerCase() === 'numeric' + ) { + typeWithSize = `${typeName}(${field.precision}, ${field.scale})`; + } + } else if (field.precision) { + if ( + typeName.toLowerCase() === 'decimal' || + typeName.toLowerCase() === 'numeric' + ) { + typeWithSize = `${typeName}(${field.precision})`; + } + } - const indexName = `"${combinedName}"`; + // Handle array types (check if the type name ends with '[]') + if (typeName.endsWith('[]')) { + typeWithSize = + typeWithSize.replace('[]', '') + '[]'; + } - // Get the properly quoted field names - const indexFieldNames = indexFields - .map((field) => - field ? `"${field.name}"` : '' - ) - .filter(Boolean); + const notNull = field.nullable ? '' : ' NOT NULL'; - return indexFieldNames.length > 0 - ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${indexFieldNames.join(', ')});` + // Handle identity generation + let identity = ''; + if ( + field.default && + field.default.includes('nextval') + ) { + // PostgreSQL already handles this with DEFAULT nextval() + } else if ( + field.default && + field.default.toLowerCase().includes('identity') + ) { + identity = ' GENERATED BY DEFAULT AS IDENTITY'; + } + + // Only add UNIQUE constraint if the field is not part of the primary key + // This avoids redundant uniqueness constraints + const unique = + !field.primaryKey && field.unique ? ' UNIQUE' : ''; + + // Handle default value using PostgreSQL specific parser + const defaultValue = + field.default && + !field.default.toLowerCase().includes('identity') + ? ` DEFAULT ${parsePostgresDefault(field)}` : ''; - }) - .filter(Boolean); - return validIndexes.length > 0 - ? `\n-- Indexes\n${validIndexes.join('\n')}` - : ''; - })() - }\n`; - }) - .filter(Boolean) // Remove empty strings (views) - .join('\n'); + // Do not add PRIMARY KEY as a column constraint - will add as table constraint + return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${serialType || typeWithSize}${serialType ? '' : notNull}${identity}${unique}${defaultValue}`; + }) + .join(',\n')}${ + primaryKeyFields.length > 0 + ? `,\n PRIMARY KEY (${primaryKeyFields + .map((f) => `"${f.name}"`) + .join(', ')})` + : '' + }\n);${ + // Add table comments + table.comments + ? `\nCOMMENT ON TABLE ${tableName} IS '${escapeSQLComment(table.comments)}';` + : '' + }${ + // Add column comments + table.fields + .filter((f) => f.comments) + .map( + (f) => + `\nCOMMENT ON COLUMN ${tableName}."${f.name}" IS '${escapeSQLComment(f.comments || '')}';` + ) + .join('') + }${ + // Add indexes only for non-primary key fields or composite indexes + // This avoids duplicate indexes on primary key columns + (() => { + const validIndexes = table.indexes + .map((index) => { + // Get the list of fields for this index + const indexFields = index.fieldIds + .map((fieldId) => { + const field = table.fields.find( + (f) => f.id === fieldId + ); + return field ? field : null; + }) + .filter(Boolean); + + // Skip if this index exactly matches the primary key fields + // This prevents creating redundant indexes + if ( + primaryKeyFields.length === + indexFields.length && + primaryKeyFields.every((pk) => + indexFields.some( + (field) => + field && field.id === pk.id + ) + ) + ) { + return ''; + } + + // Create unique index name using table name and index name + // This ensures index names are unique across the database + const safeTableName = table.name.replace( + /[^a-zA-Z0-9_]/g, + '_' + ); + const safeIndexName = index.name.replace( + /[^a-zA-Z0-9_]/g, + '_' + ); + + // Limit index name length to avoid PostgreSQL's 63-character identifier limit + let combinedName = `${safeTableName}_${safeIndexName}`; + if (combinedName.length > 60) { + // If too long, use just the index name or a truncated version + combinedName = + safeIndexName.length > 60 + ? safeIndexName.substring(0, 60) + : safeIndexName; + } + + const indexName = `"${combinedName}"`; + + // Get the properly quoted field names + const indexFieldNames = indexFields + .map((field) => + field ? `"${field.name}"` : '' + ) + .filter(Boolean); + + return indexFieldNames.length > 0 + ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${indexFieldNames.join(', ')});` + : ''; + }) + .filter(Boolean); + + return validIndexes.length > 0 + ? `\n-- Indexes\n${validIndexes.join('\n')}` + : ''; + })() + }\n`; + }) + .filter(Boolean) // Remove empty strings (views) + .join('\n'); + } // Generate foreign keys if (relationships.length > 0) { diff --git a/src/lib/data/export-metadata/export-per-type/sqlite.ts b/src/lib/data/export-metadata/export-per-type/sqlite.ts index 8a67befd..7df146d6 100644 --- a/src/lib/data/export-metadata/export-per-type/sqlite.ts +++ b/src/lib/data/export-metadata/export-per-type/sqlite.ts @@ -140,7 +140,13 @@ function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string { return typeName; } -export function exportSQLite(diagram: Diagram): string { +export function exportSQLite({ + diagram, + onlyRelationships = false, +}: { + diagram: Diagram; + onlyRelationships?: boolean; +}): string { if (!diagram.tables || !diagram.relationships) { return ''; } @@ -166,159 +172,167 @@ export function exportSQLite(diagram: Diagram): string { 'sqlite_master', ]; - // Generate table creation SQL - sqlScript += tables - .map((table: DBTable) => { - // Skip views - if (table.isView) { - return ''; - } + if (!onlyRelationships) { + // Generate table creation SQL + sqlScript += tables + .map((table: DBTable) => { + // Skip views + if (table.isView) { + return ''; + } - // Skip SQLite system tables - if (sqliteSystemTables.includes(table.name.toLowerCase())) { - return `-- Skipping SQLite system table: "${table.name}"\n`; - } + // Skip SQLite system tables + if (sqliteSystemTables.includes(table.name.toLowerCase())) { + return `-- Skipping SQLite system table: "${table.name}"\n`; + } - // SQLite doesn't use schema prefixes, so we use just the table name - // Include the schema in a comment if it exists - const schemaComment = table.schema - ? `-- Original schema: ${table.schema}\n` - : ''; - const tableName = `"${table.name}"`; + // SQLite doesn't use schema prefixes, so we use just the table name + // Include the schema in a comment if it exists + const schemaComment = table.schema + ? `-- Original schema: ${table.schema}\n` + : ''; + const tableName = `"${table.name}"`; - // Get primary key fields - const primaryKeyFields = table.fields.filter((f) => f.primaryKey); + // Get primary key fields + const primaryKeyFields = table.fields.filter( + (f) => f.primaryKey + ); - // Check if this is a single-column INTEGER PRIMARY KEY (for AUTOINCREMENT) - const singleIntegerPrimaryKey = - primaryKeyFields.length === 1 && - (primaryKeyFields[0].type.name.toLowerCase() === 'integer' || - primaryKeyFields[0].type.name.toLowerCase() === 'int'); + // Check if this is a single-column INTEGER PRIMARY KEY (for AUTOINCREMENT) + const singleIntegerPrimaryKey = + primaryKeyFields.length === 1 && + (primaryKeyFields[0].type.name.toLowerCase() === + 'integer' || + primaryKeyFields[0].type.name.toLowerCase() === 'int'); - return `${schemaComment}${ - table.comments ? formatTableComment(table.comments) : '' - }CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields - .map((field: DBField) => { - const fieldName = `"${field.name}"`; + return `${schemaComment}${ + table.comments ? formatTableComment(table.comments) : '' + }CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields + .map((field: DBField) => { + const fieldName = `"${field.name}"`; - // Handle type name - map to SQLite compatible types - const typeName = mapSQLiteType( - field.type.name, - field.primaryKey - ); + // Handle type name - map to SQLite compatible types + const typeName = mapSQLiteType( + field.type.name, + field.primaryKey + ); - // SQLite ignores length specifiers, so we don't add them - // We'll keep this simple without size info - const typeWithoutSize = typeName; + // SQLite ignores length specifiers, so we don't add them + // We'll keep this simple without size info + const typeWithoutSize = typeName; - const notNull = field.nullable ? '' : ' NOT NULL'; + const notNull = field.nullable ? '' : ' NOT NULL'; - // Handle autoincrement - only works with INTEGER PRIMARY KEY - let autoIncrement = ''; - if ( - field.primaryKey && - singleIntegerPrimaryKey && - (field.default?.toLowerCase().includes('identity') || - field.default + // Handle autoincrement - only works with INTEGER PRIMARY KEY + let autoIncrement = ''; + if ( + field.primaryKey && + singleIntegerPrimaryKey && + (field.default ?.toLowerCase() - .includes('autoincrement') || - field.default?.includes('nextval')) - ) { - autoIncrement = ' AUTOINCREMENT'; - } - - // Only add UNIQUE constraint if the field is not part of the primary key - const unique = - !field.primaryKey && field.unique ? ' UNIQUE' : ''; - - // Handle default value - Special handling for datetime() function - let defaultValue = ''; - if ( - field.default && - !field.default.toLowerCase().includes('identity') && - !field.default - .toLowerCase() - .includes('autoincrement') && - !field.default.includes('nextval') - ) { - // Special handling for quoted functions like 'datetime(\'\'now\'\')' - remove extra quotes - if (field.default.includes("datetime(''now'')")) { - defaultValue = ' DEFAULT CURRENT_TIMESTAMP'; - } else { - defaultValue = ` DEFAULT ${parseSQLiteDefault(field)}`; + .includes('identity') || + field.default + ?.toLowerCase() + .includes('autoincrement') || + field.default?.includes('nextval')) + ) { + autoIncrement = ' AUTOINCREMENT'; } - } - // Add PRIMARY KEY inline only for single INTEGER primary key - const primaryKey = - field.primaryKey && singleIntegerPrimaryKey - ? ' PRIMARY KEY' + autoIncrement - : ''; + // Only add UNIQUE constraint if the field is not part of the primary key + const unique = + !field.primaryKey && field.unique ? ' UNIQUE' : ''; - return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithoutSize}${primaryKey}${notNull}${unique}${defaultValue}`; - }) - .join(',\n')}${ - // Add PRIMARY KEY as table constraint for composite primary keys or non-INTEGER primary keys - primaryKeyFields.length > 0 && !singleIntegerPrimaryKey - ? `,\n PRIMARY KEY (${primaryKeyFields - .map((f) => `"${f.name}"`) - .join(', ')})` - : '' - }\n);\n${ - // Add indexes - SQLite doesn't support indexes in CREATE TABLE - (() => { - const validIndexes = table.indexes - .map((index) => { - // Skip indexes that exactly match the primary key - const indexFields = index.fieldIds - .map((fieldId) => { - const field = table.fields.find( - (f) => f.id === fieldId - ); - return field ? field : null; - }) - .filter(Boolean); - - // Get the properly quoted field names - const indexFieldNames = indexFields - .map((field) => - field ? `"${field.name}"` : '' - ) - .filter(Boolean); - - // Skip if this index exactly matches the primary key fields - if ( - primaryKeyFields.length === - indexFields.length && - primaryKeyFields.every((pk) => - indexFields.some( - (field) => field && field.id === pk.id - ) - ) - ) { - return ''; + // Handle default value - Special handling for datetime() function + let defaultValue = ''; + if ( + field.default && + !field.default.toLowerCase().includes('identity') && + !field.default + .toLowerCase() + .includes('autoincrement') && + !field.default.includes('nextval') + ) { + // Special handling for quoted functions like 'datetime(\'\'now\'\')' - remove extra quotes + if (field.default.includes("datetime(''now'')")) { + defaultValue = ' DEFAULT CURRENT_TIMESTAMP'; + } else { + defaultValue = ` DEFAULT ${parseSQLiteDefault(field)}`; } + } - // Create safe index name - const safeIndexName = `${table.name}_${index.name}` - .replace(/[^a-zA-Z0-9_]/g, '_') - .substring(0, 60); - - return indexFieldNames.length > 0 - ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS "${safeIndexName}"\nON ${tableName} (${indexFieldNames.join(', ')});` + // Add PRIMARY KEY inline only for single INTEGER primary key + const primaryKey = + field.primaryKey && singleIntegerPrimaryKey + ? ' PRIMARY KEY' + autoIncrement : ''; - }) - .filter(Boolean); - return validIndexes.length > 0 - ? `\n-- Indexes\n${validIndexes.join('\n')}` - : ''; - })() - }\n`; - }) - .filter(Boolean) // Remove empty strings (views) - .join('\n'); + return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithoutSize}${primaryKey}${notNull}${unique}${defaultValue}`; + }) + .join(',\n')}${ + // Add PRIMARY KEY as table constraint for composite primary keys or non-INTEGER primary keys + primaryKeyFields.length > 0 && !singleIntegerPrimaryKey + ? `,\n PRIMARY KEY (${primaryKeyFields + .map((f) => `"${f.name}"`) + .join(', ')})` + : '' + }\n);\n${ + // Add indexes - SQLite doesn't support indexes in CREATE TABLE + (() => { + const validIndexes = table.indexes + .map((index) => { + // Skip indexes that exactly match the primary key + const indexFields = index.fieldIds + .map((fieldId) => { + const field = table.fields.find( + (f) => f.id === fieldId + ); + return field ? field : null; + }) + .filter(Boolean); + // Get the properly quoted field names + const indexFieldNames = indexFields + .map((field) => + field ? `"${field.name}"` : '' + ) + .filter(Boolean); + + // Skip if this index exactly matches the primary key fields + if ( + primaryKeyFields.length === + indexFields.length && + primaryKeyFields.every((pk) => + indexFields.some( + (field) => + field && field.id === pk.id + ) + ) + ) { + return ''; + } + + // Create safe index name + const safeIndexName = + `${table.name}_${index.name}` + .replace(/[^a-zA-Z0-9_]/g, '_') + .substring(0, 60); + + return indexFieldNames.length > 0 + ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS "${safeIndexName}"\nON ${tableName} (${indexFieldNames.join(', ')});` + : ''; + }) + .filter(Boolean); + + return validIndexes.length > 0 + ? `\n-- Indexes\n${validIndexes.join('\n')}` + : ''; + })() + }\n`; + }) + .filter(Boolean) // Remove empty strings (views) + .join('\n'); + } // Generate table constraints and triggers for foreign keys // SQLite handles foreign keys differently - we'll add them with CREATE TABLE statements // But we'll also provide individual ALTER TABLE statements as comments for reference diff --git a/src/lib/data/export-metadata/export-sql-script.ts b/src/lib/data/export-metadata/export-sql-script.ts index 2d5f5434..bf9cf7e1 100644 --- a/src/lib/data/export-metadata/export-sql-script.ts +++ b/src/lib/data/export-metadata/export-sql-script.ts @@ -36,10 +36,12 @@ export const exportBaseSQL = ({ diagram, targetDatabaseType, isDBMLFlow = false, + onlyRelationships = false, }: { diagram: Diagram; targetDatabaseType: DatabaseType; isDBMLFlow?: boolean; + onlyRelationships?: boolean; }): string => { const { tables, relationships } = diagram; @@ -50,16 +52,16 @@ export const exportBaseSQL = ({ if (!isDBMLFlow && diagram.databaseType === targetDatabaseType) { switch (diagram.databaseType) { case DatabaseType.SQL_SERVER: - return exportMSSQL(diagram); + return exportMSSQL({ diagram, onlyRelationships }); case DatabaseType.POSTGRESQL: - return exportPostgreSQL(diagram); + return exportPostgreSQL({ diagram, onlyRelationships }); case DatabaseType.SQLITE: - return exportSQLite(diagram); + return exportSQLite({ diagram, onlyRelationships }); case DatabaseType.MYSQL: case DatabaseType.MARIADB: - return exportMySQL(diagram); + return exportMySQL({ diagram, onlyRelationships }); default: - return exportPostgreSQL(diagram); + return exportPostgreSQL({ diagram, onlyRelationships }); } }